diff --git a/docs/js-usage.md b/docs/js-usage.md
index d51d7466..d6e97821 100644
--- a/docs/js-usage.md
+++ b/docs/js-usage.md
@@ -414,6 +414,66 @@ S.arrayMinLength(S.array(S.string) 5); // Array must be 5 or more items long
S.arrayLength(S.array(S.string) 5); // Array must be exactly 5 items long
```
+### Unnest
+
+```ts
+const schema = S.unnest(
+ S.schema({
+ id: S.string,
+ name: S.nullable(S.string),
+ deleted: S.boolean,
+ })
+);
+
+const value = S.reverseConvertOrThrow(
+ [
+ { id: "0", name: "Hello", deleted: false },
+ { id: "1", name: undefined, deleted: true },
+ ],
+ schema
+);
+// [["0", "1"], ["Hello", null], [false, true]]
+```
+
+The helper function is inspired by the article [Boosting Postgres INSERT Performance by 2x With UNNEST](https://www.timescale.com/blog/boosting-postgres-insert-performance). It allows you to flatten a nested array of objects into arrays of values by field.
+
+The main concern of the approach described in the article is usability. And ReScript Schema completely solves the problem, providing a simple and intuitive API that is even more performant than `S.array`.
+
+
+
+
+Checkout the compiled code yourself:
+
+
+```javascript
+(i) => {
+ let v1 = [new Array(i.length), new Array(i.length), new Array(i.length)];
+ for (let v0 = 0; v0 < i.length; ++v0) {
+ let v3 = i[v0];
+ try {
+ let v4 = v3["name"],
+ v5;
+ if (v4 !== void 0) {
+ v5 = v4;
+ } else {
+ v5 = null;
+ }
+ v1[0][v0] = v3["id"];
+ v1[1][v0] = v5;
+ v1[2][v0] = v3["deleted"];
+ } catch (v2) {
+ if (v2 && v2.s === s) {
+ v2.path = "" + "[\"'+v0+'\"]" + v2.path;
+ }
+ throw v2;
+ }
+ }
+ return v1;
+};
+```
+
+
+
## Tuples
Unlike arrays, tuples have a fixed number of elements and each element can have a different type.
diff --git a/docs/rescript-usage.md b/docs/rescript-usage.md
index a6a7b34d..3d754a26 100644
--- a/docs/rescript-usage.md
+++ b/docs/rescript-usage.md
@@ -39,6 +39,7 @@
- [Enums](#enums)
- [`array`](#array)
- [`list`](#list)
+ - [`unnest`](#unnest)
- [`tuple`](#tuple)
- [`tuple1` - `tuple3`](#tuple1---tuple3)
- [`dict`](#dict)
@@ -892,6 +893,60 @@ let schema = S.list(S.string)
The `S.list` schema represents an array of data of a specific type which is transformed to ReScript's list data-structure.
+### **`unnest`**
+
+`S.t<'value> => S.t>`
+
+```rescript
+let schema = S.unnest(S.schema(s => {
+ id: s.matches(S.string),
+ name: s.matches(S.null(S.string)),
+ deleted: s.matches(S.bool),
+}))
+
+[{id: "0", name: Some("Hello"), deleted: false}, {id: "1", name: None, deleted: true}]->S.reverseConvertOrThrow(schema)
+// [["0", "1"], ["Hello", null], [false, true]]
+```
+
+The helper function is inspired by the article [Boosting Postgres INSERT Performance by 2x With UNNEST](https://www.timescale.com/blog/boosting-postgres-insert-performance). It allows you to flatten a nested array of objects into arrays of values by field.
+
+The main concern of the approach described in the article is usability. And ReScript Schema completely solves the problem, providing a simple and intuitive API that is even more performant than `S.array`.
+
+
+
+
+Checkout the compiled code yourself:
+
+
+```javascript
+(i) => {
+ let v1 = [new Array(i.length), new Array(i.length), new Array(i.length)];
+ for (let v0 = 0; v0 < i.length; ++v0) {
+ let v3 = i[v0];
+ try {
+ let v4 = v3["name"],
+ v5;
+ if (v4 !== void 0) {
+ v5 = v4;
+ } else {
+ v5 = null;
+ }
+ v1[0][v0] = v3["id"];
+ v1[1][v0] = v5;
+ v1[2][v0] = v3["deleted"];
+ } catch (v2) {
+ if (v2 && v2.s === s) {
+ v2.path = "" + "[\"'+v0+'\"]" + v2.path;
+ }
+ throw v2;
+ }
+ }
+ return v1;
+};
+```
+
+
+
### **`tuple`**
`(S.Tuple.s => 'value) => S.t<'value>`
diff --git a/package.json b/package.json
index e0645665..c25a075c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "rescript-schema",
- "version": "9.0.1",
+ "version": "9.1.0",
"description": "🧬 The fastest parser in the entire JavaScript ecosystem with a focus on small bundle size and top-notch DX",
"keywords": [
"ReScript",
diff --git a/packages/tests/src/core/S_test.ts b/packages/tests/src/core/S_test.ts
index c506c654..f401746d 100644
--- a/packages/tests/src/core/S_test.ts
+++ b/packages/tests/src/core/S_test.ts
@@ -1708,6 +1708,44 @@ test("Tuple types", (t) => {
t.pass();
});
+test("Unnest schema", (t) => {
+ const schema = S.unnest(
+ S.schema({
+ id: S.string,
+ name: S.nullable(S.string),
+ deleted: S.boolean,
+ })
+ );
+
+ const value = S.reverseConvertOrThrow(
+ [
+ { id: "0", name: "Hello", deleted: false },
+ { id: "1", name: undefined, deleted: true },
+ ],
+ schema
+ );
+
+ let expected: typeof value = [
+ ["0", "1"],
+ ["Hello", null],
+ [false, true],
+ ];
+
+ t.deepEqual(value, expected);
+
+ expectType<
+ SchemaEqual<
+ typeof schema,
+ {
+ id: string;
+ name: string | undefined;
+ deleted: boolean;
+ }[],
+ (string[] | boolean[] | (string | null)[])[]
+ >
+ >(true);
+});
+
test("Tuple with transform to object", (t) => {
let pointSchema = S.tuple((s) => {
s.tag(0, "point");
diff --git a/packages/tests/src/core/S_unnest_test.res b/packages/tests/src/core/S_unnest_test.res
index 0170d724..f888da36 100644
--- a/packages/tests/src/core/S_unnest_test.res
+++ b/packages/tests/src/core/S_unnest_test.res
@@ -42,6 +42,21 @@ test("Successfully parses and reverse converts a simple object with unnest", t =
%raw(`[["a", "b"], [0, 1]]`),
(),
)
+
+ let example = S.unnest(
+ S.schema(s =>
+ {
+ "id": s.matches(S.string),
+ "name": s.matches(S.null(S.string)),
+ "deleted": s.matches(S.bool),
+ }
+ ),
+ )
+ t->U.assertCompiledCode(
+ ~schema=example,
+ ~op=#ReverseConvert,
+ `i=>{let v1=[new Array(i.length),new Array(i.length),new Array(i.length),];for(let v0=0;v0 {
diff --git a/src/S.d.ts b/src/S.d.ts
index 6f860296..30603941 100644
--- a/src/S.d.ts
+++ b/src/S.d.ts
@@ -204,6 +204,15 @@ export const array: