Releases: DZakh/rescript-schema
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
v8.2.0
What's Changed
- Add
S.enum
helper - Improve serializing by using experimental reverse schema under the hood by @DZakh in #89
Note: The S.union serializing logic changed in the release. Schemas are not guaranteed to be validated in the order they are passed to S.union. They are grouped by the input data type to optimise performance and improve error message. Schemas with unknown data typed validated the last.
Full Changelog: v8.1.0...v8.2.0
v8.1.0
🆕 Tag shorthand for Js/Ts api object schema
Besides passing schemas for values in S.object
, you can also pass any Js value.
const meSchema = S.object({
id: S.number,
name: "Dmitry Zakharov",
age: 23,
kind: "human" as const,
metadata: {
description: "What?? Even an object with NaN works! Yes 🔥",
money: NaN,
},
});
This is a shorthand for the advanced s.tag
and useful for discriminated unions.
// TypeScript type for reference:
// type Shape =
// | { kind: "circle"; radius: number }
// | { kind: "square"; x: number }
// | { kind: "triangle"; x: number; y: number };
const shapeSchema = S.union([
S.object({
kind: "circle" as const,
radius: S.number,
}),
S.object({
kind: "square" as const,
x: S.number,
}),
S.object({
kind: "triangle" as const,
x: S.number,
y: S.number,
}),
]);
Full Changelog: v8.0.3...v8.1.0
v8.0.3
- Fix TS type for
assert
- Fix TS type for
s.fail
- Update regular expressions used for
email
anduuid
validations. It adds support for uuidv7 validation.
Full Changelog: v8.0.2...v8.0.3
v8.0.2
- Added
S.recursive
support for JS/TS API
Full Changelog: v8.0.1...v8.0.2