Releases: DZakh/rescript-schema
v9.3.0
New Feature
S.coerce
(S.t<'from>, S.t<'to>) => S.t<'to>
This very powerful API allows you to coerce another data type in a declarative way. Let's say you receive a number that is passed to your system as a string. For this S.coerce
is the best fit:
let schema = S.string->S.coerce(S.float)
"123"->S.parseOrThrow(schema) //? 123.
"abc"->S.parseOrThrow(schema) //? throws: Failed parsing at root. Reason: Expected number, received "abc"
// Reverse works correctly as well 🔥
123.->S.reverseConvertOrThrow(schema) //? "123"
Currently, ReScript Schema supports the following coercions (🔄 means reverse support):
- from
string
tostring
🔄 - from
string
to literalstring
,boolean
,number
,bigint
null
,undefined
,NaN
🔄 - from
string
toboolean
🔄 - from
string
toint32
🔄 - from
string
tonumber
🔄 - from
string
tobigint
🔄 - from
int32
tonumber
There are plans to add more support in future versions and make it extensible.
Other Changes
- Renamed
S.to
->S.shape
.S.to
was deprecated and will be removed in v10. - Exposed
S.shape
to JS/TS API - Removed "reverse" from the error message text
Full Changelog: v9.2.3...v9.3.0
v9.2.3
Fix reverse convert for object tag literal fields with preprocessed schema:
let schema = S.object(s => {
let _ = s.nested("nested").field(
"tag",
S.literal("value")->S.preprocess(_ => {serializer: v => "_" ++ v->Obj.magic}),
)
})
t->Assert.deepEqual(
()->S.reverseConvertOrThrow(schema),
%raw(`{"nested":{"tag":"_value"}}`),
)
Full Changelog: v9.2.2...v9.2.3
v9.2.2
Fix CommonJs build.
Full Changelog: v9.2.0...v9.2.2
v9.2.0
What's Changed
- http://standardschema.dev spec support
- TS/JS users are not required to install 'rescript' anymore
- Add ArkType to comparison table
- Update object type validation
- Rename .bs.js file extention to .res.js for generated files
Full Changelog: v9.1.0...v9.2.0
v9.1.0
New S.unnest
helper
S.t<'value> => S.t<array<'value>>
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. 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:
(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;
};
Other changes
- Improved
S.array
performance - Fixed reverse operations for
S.schema
used inside of thes.flatten
Full Changelog: v9.0.1...v9.1.0
v9.0.1
Fixed Array schema name.
Full Changelog: v9.0.0...v9.0.1
v9.0.0
ReScript Schema V9 is a big step for the library. Even though there are not many changes in the ReScript API, the version comes with a completely new mental model, an internal redesign for enhanced operations flexibility, and a drastic improvement for the JS/TS API.
New mental model 🧬
The main idea of ReScript Schema-like libraries is to create a schema with code that describes a desired data structure. Then, you can use the schema to parse data incoming to your application, derive types, create JSON-Schema, and many more. This is how other libraries, like Zod, Valibot, Superstruct, yup, etc, work.
Even though ReScript Schema is now a mature and powerful library for JS users, I originally created it specifically to parse data in ReScript applications, with the core functionality to automatically transform data to the ReScript representation and back to the original format. This is how the advanced object API was born - the primary tool of ReScript Schema up till V9.
let filmSchema = S.object(s => {
id: s.field("Id", S.float),
title: s.field("Title", S.string),
tags: s.fieldOr("Tags", S.array(S.string), []),
rating: s.field(
"Rating",
S.union([
S.literal(GeneralAudiences),
S.literal(ParentalGuidanceSuggested),
S.literal(ParentalStronglyCautioned),
S.literal(Restricted),
]),
),
deprecatedAgeRestriction: s.field("Age", S.option(S.int)->S.deprecate("Use rating instead")),
})
Here, you can see features like declarative validation, fields renaming and automatic transformation/mapping in a single schema definition. And the schema will produce the most optimized function to parse or convert your value back to the original format using eval
.
What's changed and why?
As I said before, initially, the library covered two prominent use cases: parsing & serializing. But with time, when ReScript Schema got used more extensively, it wasn't enough anymore. For example, you'd want only to apply transformations without validation, validate the data before serializing, assert with the most performance without allocations for output value, or parse JSON data, transform it, and convert the result to JSON again. And V9 introduces the S.compile
function to create custom operations with 100 different combinations 💪
To make things even more flexible, all operations became one-directional. This might be slightly confusing because it means there's no serializing anymore. But V9 comes with a new, powerful concept of reversing your schema. Since we have 50 different one-directional operations, we now have 50 more for the opposite direction. So I hope, it's a good explanation why serializeOrRaiseWith
was removed.
To keep the DX on the same level, besides S.compile
+ S.reverse
functions, there are some built-in operations like reverseConvertOrThrow
, which is equivalent to the removed serializeOrRaiseWith
.
Another hidden power of the reverse feature is that now it's possible to get the type information of the schema output, allowing one to compile even more optimized schemas and build even more advanced tools.
TS to the moon 🚀
For me, the custom compilation and reverse mental model are the biggest changes, but if you're a TypeScript user, you'll find another one to be more exciting in the v9 release. And yes, I know there are pretty many breaking changes, but creating a schema was never as smooth.
Before:
const shapeSchema = S.union([
S.object({
kind: S.literal("circle"),
radius: S.number,
}),
S.object({
kind: S.literal("triangle"),
x: S.number,
y: S.number,
}),
]);
After:
const shapeSchema = S.union([
{
kind: "circle" as const,
radius: S.number,
},
{
kind: "triangle" as const, // Or you can have S.schema("triangle")
x: S.number,
y: S.number,
},
]);
In V9 you don't need to care whether you work with an object, tuple, or a literal - simply use S.schema
for all cases. If you have an enum, discriminated union, or some variadic union - simply use S.union
. There's a single API, and ReScript Schema will automatically compile the most optimized operation for you.
Clean up built-in operations 🧹
If you're familiar with ReScript Schema's older versions, the built-in operations look somehow different. For example, serializeOrRaiseWith
became reverseConvertOrThrow
. So there are changes you should know about:
- All operations are now throwing; there's no result-based API. As an alternative for ReScript I recommend using try/catch with the S.Raised exception. For TS users, there's a new S.safe API for convenient error handling.
- TS and ReScript API are now the same, which is easier to maintain, interop between languages and work in mixed codebases.
- The
Raise
suffix is renamed toThrow
since this makes more sense in Js ecosystems, and there are plans to rename theraise
function tothrow
in the next ReScript versions. - The
With
suffix is removed to save some space. Note that the operation is still data-first, and the schema should be a second argument.
Other changes you should know about 👀
In the release, S.schema
got a noticeable boost. Besides becoming TS users' main API now, I recommend using it in ReScript by default when you don't need transformations. Previously, it was simply a wrapper over advanced S.object
and S.tuple
, but it got its own implementation, which allows creating schemas faster, as well as including optimizations for compiled operations:
- If you use
S.schema
for strict objects without transformed fields, the incoming object won't be recreated. - If you use
S.schema
for tuples without transformed items, the incoming array won't be recreated.
Also, you can pass the object schema created with S.schema
to the new Advanced Object API for nested fields coming in as a replacement for s.nestedField
:
let entityDataSchema = S.schema(s => {
name: s.matches(S.string),
age: s.matches(S.int),
})
let entitySchema = S.object(s => {
// Schema created with S.object wouldn't work here 👌
let {name, age} = s.nested("data").flatten(entityDataSchema)
{
id: s.nested("data").field("id", S.string),
name,
age,
}
})
Besides the runtime improvements for operations of S.schema
, there are a few more, which were possible thanks to the ability to know the output type information:
- The union of objects will use literal fields to choose which schema it should use for parsing. Previously, it used to run operations for every schema of the union and return the result of the first non-failing one. The approach was slow, provided worse error messages, and didn't work for reversed operations. In V9 all the problems are solved.
- Improved optional operations by doing the
Caml_option.valFromOption
call only when it's truly needed.
As a cherry on top of the cake, schema names became more readable, making error messages even better:
-Failed parsing at root. Reason: Expected Object({"foo":Option(String)}), received 123
+Failed parsing at root. Reason: Expected { foo: string | undefined; }, received 123
Thanks for reading the high-level overview of the release. Contact me on GitHub or X if you have any questions 😁 And enjoy using ReScript Schema 🚀
Full Changelog: v8.1.0...v9.0.0
v9.0.0-rc.1
Full Changelog: v8.4.0...v9.0.0-rc.1
v8.4.0
TS API Features
Shorthand syntax for S.schema
const loginSchema = S.schema({
email: S.email(S.string),
password: S.stringMinLength(S.string, 8),
});
It's going to replace S.object
in V9.
Shorthand syntax for S.union
const shapeSchema = S.union([
{
kind: "circle" as const,
radius: S.number,
},
{
kind: "square" as const,
x: S.number,
},
{
kind: "triangle" as const,
x: S.number,
y: S.number,
},
]);
Added function-based operation instead of method-based
- parseWith
- parseAsyncWith
- convertWith
- convertToJsonStringWith
- assertWith
They throw exceptions by default. You can use the S.safe
and S.safeAsync
to turn them into result objects.
Deprecations in favor of a new name
S.variant
->S.to
S.assertAnyWith
->S.assertWith
Temporary Regressions/Breaking changes
S.to
/S.variant
doesn't allow to destructure tuples anymores.flatten
stopped working withS.schema
Reverse conversion (serializing) improvement
Now, it's possible to convert back to object schemas where a single field is used multiple times.
Bug Fix
Fix parsing of a tuple nested inside of an object schema.
Full Changelog: v8.3.0...v8.4.0
v8.3.0
What's Changed
- Added
S.bigint
,S.unwrap
,S.compile
,S.removeTypeValidation
,S.convertAnyWith
,S.convertAnyToJsonWith
,S.convertAnyToJsonStringWith
,S.convertAnyAsyncWith
. See the docs for more info. - Deprecated
S.isAsyncParse
in favor ofS.isAsync
- Deprecated
S.assertOrRaiseWith
in favor ofS.assertAnyWith
- Deprecated
S.parseOrRaiseWith
,S.parseAnyOrRaiseWith
,S.serializeOrRaiseWith
,S.serializeToUnknownOrRaiseWith
,S.serializeToJsonStringOrRaiseWith
. Use safe versions instead, together with the newS.unwrap
helper. If you care about performance, I recommend usingS.compile
in this case.
New Contributors
- @WhyThat made their first contribution in #90 by adding PPX support for macOS x64
- @cknitt made their first contribution in #93 by improving compatibility with ReScript v12
Full Changelog: v8.2.0...v8.3.0