Skip to content

Releases: DZakh/rescript-schema

v9.3.0

18 Feb 08:03
Compare
Choose a tag to compare

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 to string 🔄
  • from string to literal string, boolean, number, bigint null, undefined, NaN 🔄
  • from string to boolean 🔄
  • from string to int32 🔄
  • from string to number 🔄
  • from string to bigint 🔄
  • from int32 to number

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

13 Feb 09:54
Compare
Choose a tag to compare

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

13 Feb 09:22
Compare
Choose a tag to compare

Fix CommonJs build.

Full Changelog: v9.2.0...v9.2.2

v9.2.0

08 Feb 22:59
Compare
Choose a tag to compare

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

22 Jan 09:21
Compare
Choose a tag to compare

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 the s.flatten

Full Changelog: v9.0.1...v9.1.0

v9.0.1

09 Jan 07:40
Compare
Choose a tag to compare

Fixed Array schema name.

Full Changelog: v9.0.0...v9.0.1

v9.0.0

09 Jan 07:39
Compare
Choose a tag to compare

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 to Throw since this makes more sense in Js ecosystems, and there are plans to rename the raise function to throw 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

06 Dec 09:56
Compare
Choose a tag to compare
v9.0.0-rc.1 Pre-release
Pre-release

Full Changelog: v8.4.0...v9.0.0-rc.1

v8.4.0

17 Oct 07:22
Compare
Choose a tag to compare
v8.4.0 Pre-release
Pre-release

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 anymore
  • s.flatten stopped working with S.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

27 Sep 09:08
Compare
Choose a tag to compare
v8.3.0 Pre-release
Pre-release

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 of S.isAsync
  • Deprecated S.assertOrRaiseWith in favor of S.assertAnyWith
  • Deprecated S.parseOrRaiseWith, S.parseAnyOrRaiseWith, S.serializeOrRaiseWith, S.serializeToUnknownOrRaiseWith, S.serializeToJsonStringOrRaiseWith. Use safe versions instead, together with the new S.unwrap helper. If you care about performance, I recommend using S.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