Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Dmitry Zakharov
Dmitry Zakharov

Posted on • Edited on

     

JavaScript schema library from the Future 🧬

ReScript Schema - The fastest parser in the entire JavaScript ecosystem with a focus on small bundle size and top-notch DX.

Why did you not hear about it then, and why should you learn about it now? I started developing the library three years ago, and today, it's at a point others have yet to achieve. I'll prove it in the article, but before we start, I'd like to answer a few questions you might already have.

What's a parser?

One of the most basic applications of ReScript Schema is parsing - Accepting unknown JavaScript data, validating it, and returning the result of your desired type. There are dozens of such libraries, and the most popular ones areZod,Valibot,Runtypes,Arktype,Typia,Superstruct,Effect Schema, and more. Also, even though this is slightly different, validation libraries likeAjv,Yup, and others also stand really close.

Is ReScript Schema faster than all of them?

Yes. It's ~100 times faster thanZod and on par withTypia orArktype (benchmark). But often, besides validation, you want to transform data incoming to your system, and here, ReScript Schema overperforms any solution existing in the JavaScript ecosystem.

What's ReScript? Isn't the library for JavaScript/TypeScript?

ReScript is a robustly typed language that compiles to efficient and human-readable JavaScript. And yes, ReScript Schema is written in ReScript, but it also has a really nice JavaScript API with TS types. You don't need to install or run any compiler;npm i rescript-schema is all you need.
It makes ReScript Schema support 3 languages - JavaScript, TypeScript, and ReScript. This is especially nice when you mix TypeScript and ReScript in a single codebase 👌

Are there trade-offs?

Yes. To maximize DX, performance, and bundle size while keeping the library fully runtime, I've decided to useeval under the hood. You needn't worry about the code's dangerous execution, but some environments, like Cloudflare Workers, won't work. In 99% of cases, you don't need to be concerned about this. I just think it's my duty as a creator to let you know about the 1% beforehand.

What's the plan?

I'm going to provide an overview of the basic ReScript Schema API and mental model. Then, we'll discuss what makes it stand out from millions of similar libraries (and this is not only performance). I'll also look at some more advanced use cases and discuss the ecosystem, performance, and where it stands with other libraries.

I hope you'll enjoy it. 😊

Follow me onX to learn more about programming stuff I'm cooking.

Parsing / Validating

Let's start with the most basic use case of ReScript Schema. By the way, if you don't know the difference between parsing (sometimes called decoding) and validating, here's agood article from Zod's docs. If you're curious about when and why you need to parse data in your application, let me know in the comments. I can write a big article about it, but for now, I assume you are already familiar with the concept.

Let's finally take a look at the code. I'll go with the TypeScript example first, so it's more familiar for most readers. Everything starts with defining a schema of the data you expect:

import*asSfrom"rescript-schema";constfilmSchema=S.schema({id:S.number,title:S.string,tags:S.array(S.string),rating:S.union(["G","PG","PG13","R"])})
Enter fullscreen modeExit fullscreen mode

The schema here is like a type definition that exists in runtime. If you hover over thefilmSchema, you'll see the following type:

S.Schema<{id:number;title:string;tags:string[];rating:"G"|"PG"|"PG13"|"R";}>
Enter fullscreen modeExit fullscreen mode

This is aSchema type that inferred the film object definition. I recommend extracting its value into its own type. This way, you'll have the schema as the source of truth and theFilm type always matching the schema:

typeFilm=S.Output<typeoffilmSchema>
Enter fullscreen modeExit fullscreen mode

After we've defined ourFilm type using the schema, we can parse unknown data entering our application to guarantee that it matches what we expect:

S.parseOrThrow({id:1,title:"My first film",tags:["Loved"],rating:"S",},filmSchema,);//? Throws RescriptSchemaError with message `Failed parsing at ["rating"]. Reason: Expected "G" | "PG" | "PG13" | "R", received "S"`S.parseOrThrow(validDataWithUnknownType,filmSchema)//? Returns value of the Film type// If you don't want to throw, you can wrap the operations in S.safe and get S.Result as a return valueS.safe(()=>S.parseOrThrow(data,filmSchema))
Enter fullscreen modeExit fullscreen mode

Done! We have valid data here 🙌

Some experienced users may have noticed that the API is similar toValibot, but with a unique flavor.

You can useS.schema for objects, tuples, and literals. For any kind of union, there'sS.union; even if it's adiscriminated one, the parser will perform in the most optimized way. I personally have seen this kind of DX only inArkType so far.

Also, there are no annoying parentheses; the parse function explicitly says it can throw, and thanks to the modular design, the library tree-shaking is very good.

Package size

Since I mentioned tree-shaking, I'd like to quickly note about the package size. The bundle size is an essential metric for a web application, and I'd like to share how ReScript Schema is doing here in comparison with other libraries:

rescript-schema@9.2.2Zod@3.24.1Valibot@1.0.0-beta.14ArkType@2.0.4
Total size (minified + gzipped)12.7 kB15.2 kB12.3 kB40.8 kB
Example size (minified + gzipped)5.14 kB14.5 kB1.39 kB40.7 kB
PlaygroundLinkLinkLinkLink

It's not as amazing as Valibot, but ReScript Schema is definitely doing good here. If we compare ReScript Schema to libraries that have similar performance, they all use the code generation approach (besides ArkType). This means it'll start small, but for every new type, more and more code will be added to your bundle, rapidly increasing the application size.

Parsing using ReScript

Even though I want to make ReScript Schema popular for TS developers, ReScript is still the library's main user base, so I'll also include examples of it.

Compared to TypeScript, the type system in ReScript is much simpler; you literally can't do any type gymnastics in it. Together withnominal typing, it's getting impossible to extract thefilm type from the schema (even though it can infer it). But there's a built-in way to prevent boilerplate code in ReScript. You can use ReScript Schema PPX to generate schemas for your types automatically. Just annotate them with@schema attribute.

@schematyperating=|@as("G")GeneralAudiences|@as("PG")ParentalGuidanceSuggested|@as("PG13")ParentalStronglyCautioned|@as("R")Restricted@schematypefilm={id:float,title:string,tags:array<string>,rating:rating,}
Enter fullscreen modeExit fullscreen mode

Does therating type look scary to you? Don't worry, this is a ReScriptVariant, which is such a nice way to describe any kind of union. Also, you can use@as and give a better name to the ratings while preserving the original short values in runtime.

Although PPX is nice, you can always code without it:

typerating=|@as("G")GeneralAudiences|@as("PG")ParentalGuidanceSuggested|@as("PG13")ParentalStronglyCautioned|@as("R")Restrictedtypefilm={id:float,title:string,tags:array<string>,rating:rating,}letfilmSchema=S.schema(s=>{id:s.matches(S.number),title:s.matches(S.string),tags:s.matches(S.array(S.string)),rating:s.matches(S.union([GeneralAudiences,ParentalGuidanceSuggested,ParentalStronglyCautioned,Restricted]))})
Enter fullscreen modeExit fullscreen mode

The TS API admittedly wins here since we don't need to calls.matches to make type system happy, but when it comes to parsing ReScript takes it back with thePipe Operator andPattern Matching on exceptions:

{"id":1,"title":"My first film","tags":["Loved"],"rating":"S",}->S.parseOrThrow(filmSchema)//? Throws RescriptSchemaError with message `Failed parsing at ["rating"]. Reason: Expected "G" | "PG" | "PG13" | "R", received "S"`validDataWithUnknownType->S.parseOrThrow(filmSchema)//? Returns value of the film type// If you don't want to throw, you can match on the S.Raised exception and return the result type. There's no S.safe API like in TypeScript, since you can do better with the language itself!switchdata->S.parseOrThrow(filmSchema){|film=>Ok(film)|exceptionS.Raised(error)=>Error(error)}
Enter fullscreen modeExit fullscreen mode

Unique Features

After we covered the most basic use case, let's move on to the things that make ReScript Schema special 🔥

Changing shape and field names

Let's imagine working with a weird REST API with poorly named fields in PascalCase, where data is randomly nested in objects or tuples. But we can't change the backend, so at least we want to transform data to a more convenient format for our application. In ReScript Schema you can make it in a declarative way, which will result in the most possibly performant operation:

constfilmSchema=S.object((s)=>({id:s.field("Id",S.number),title:s.nested("Meta").field("Title",S.string),tags:s.field("Tags_v2",S.array(S.string)),rating:s.field("Rating",S.schema([S.union(["G","PG","PG13","R"])]))[0],}));S.parseOrThrow({Id:1,Meta:{Title:"My first film",},Tags_v2:["Loved"],Rating:["G"],},filmSchema);//? { id: 1, title: "My first film", tags: ["Loved"], rating: "G" }
Enter fullscreen modeExit fullscreen mode

Looks scary? Let's dive in. First of all, every schema hasInput andOutput. Quite often, they are equal, and during parsing, the library only validates thatInput has the correct type and returns it immediately. Although there are ways to change the expectedOutput type like we do in the example above. For comparison, let's take a look at how you'd usually achieve the same with other schema libraries:

constfilmSchema=S.transform(S.schema({Id:S.number,Meta:{Title:S.string,},Tags_v2:S.array(S.string),Rating:S.schema([S.union(["G","PG","PG13","R"])]),}),(input)=>({id:input.Id,title:input.Meta.Title,tags:input.Tags_v2,rating:input.Rating[0],}));
Enter fullscreen modeExit fullscreen mode

This is still ReScript Schema, but we useS.transform to manually transform theInput type. You can find this kind of API in many other schema libraries. What's good about the example is that you can clearly see that we use our schema to declaratively describe what the data incoming to our system looks like, and then we transform it to what's convenient for us to work with. In a way, the schema here is similar to a contract between the client and the server that returns the object in response.

In the advancedS.object example, which I showed first, we combine a declarative description of theInput type with a transformation to theOutput type. And this enables one more thing besides shorter code and a performance boost.

Reverse Parsing (aka serializing/decoding)

Decoding is present in many libraries from other languages, but it's not very common in the JS ecosystem. This is a big loss because the ability to perform operations in the reverse direction is the most powerful feature I personally find.

If it's unclear what I mean, in other popular JavaScript schema libraries, you can only parseInput toOutput types. While in ReScript Schema you can easily parseOutput toInput using the same schema. Or only perform the conversion logic since theOutput type usually doesn't require validation.

Do you remember ourfilmSchema usingS.object to rename fields? Let's say we want to send a POST request with the film entity, and the server also expects the weirdly cased data structure it initially sent to us. Here is how we deal with it:

// The same schema from aboveconstfilmSchema=S.object((s)=>({id:s.field("Id",S.number),title:s.nested("Meta").field("Title",S.string),tags:s.field("Tags_v2",S.array(S.string)),rating:s.field("Rating",S.schema([S.union(["G","PG","PG13","R"])]))[0],}));S.reverseConvertOrThrow({id:1,title:"My first film",tags:["Loved"],rating:"G"},filmSchema)//? { Id: 1, Meta: { Title: "My first film" }, Tags_v2: ["Loved"], Rating: ["G"] }
Enter fullscreen modeExit fullscreen mode

Sweet! Isn't it? And even though I want to talk more about performance a little bit later, I can't stop myself from sharing the code it evaluates under the hood:

(i)=>{letv0=i["tags"];return{Id:i["id"],Meta:{Title:i["title"]},Tags_v2:v0,Rating:[i["rating"]],};};
Enter fullscreen modeExit fullscreen mode

I think most people would write slower code by hand 😅

Reverse

TheS.reverseConvertOrThrow is one of the reverse cases I use daily in my work, but this is actually just a shorthand ofS.convertOrThrow andS.reverse you can use separately.

S.reverse - this is what allows you to take yourSchema<Input, Output> and turn it intoSchema<Output, Input>.

It may sound quite dull, but compared to the commonly used parser/serializer or encoder/decoder approach, here you get an actual schema you can use the same way as the original one without any limitations.

If you want, you can parse output with/without data validation, generate JSON Schema, perform optimized comparison and hashing, or use the data representation in runtime for any custom logic.

As a fruit of the ability to knowInput andOutput data types in runtime, ReScript Schema has a very powerful coercion API.

constschema=S.coerce(S.string,S.bigint)S.parseOrThrow("123",schema)//? 123nS.reverseConvertOrThrow(123n,schema)//? "123"
Enter fullscreen modeExit fullscreen mode

Pass any schemas toS.coerce that you want to coerce from and to, and ReScript Schema will figure out the rest.

And this has not been implemented yet, but with the API, it'll also be possible to achieve 2x faster JSON.stringify(). Likefast-json-stringify does and maybe even faster 😎

100 Operations

If you want the best possible performance or the built-in operations don't cover your specific use case, you can use S.compile to create fine-tuned operation functions.

constoperation=S.compile(S.string,"Any","Assert","Async");//? (input: unknown) => Promise<void>awaitoperation("Hello world!");
Enter fullscreen modeExit fullscreen mode

In the example above, we've created an async assert operation, which is not available by default.

With the API, you can get 100 different operation combinations, each of which might make sense for your specific use case. This is likeparser in Valibot, but multiplied by 💯.

Performance Comparison

As I mentioned in the beginning, ReScript Schema is the fastest. Now I'll explain why 🔥

Also, you can use the big communitybenchmark to confirm yourself. If you seeTypia overperforming ReScript Schema, I have a take on it too 😁

First of all, the biggest advantage of ReScript Schema is its very clever library core, which builds the most possibly optimized operations usingeval. I have already shown before how the operation code looks for reverse conversion; here's thefilmSchema parse operation code:

(i)=>{if(typeofi!=="object"||!i){e[7](i);}letv0=i["Id"],v1=i["Meta"],v3=i["Tags_v2"],v7=i["Rating"];if(typeofv0!=="number"||Number.isNaN(v0)){e[0](v0);}if(typeofv1!=="object"||!v1){e[1](v1);}letv2=v1["Title"];if(typeofv2!=="string"){e[2](v2);}if(!Array.isArray(v3)){e[3](v3);}for(letv4=0;v4<v3.length;++v4){letv6=v3[v4];try{if(typeofv6!=="string"){e[4](v6);}}catch(v5){if(v5&&v5.s===s){v5.path='["Tags_v2"]'+'["'+v4+'"]'+v5.path;}throwv5;}}if(!Array.isArray(v7)||v7.length!==1){e[5](v7);}letv8=v7["0"];if(v8!=="G"){if(v8!=="PG"){if(v8!=="PG13"){if(v8!=="R"){e[6](v8);}}}}return{id:v0,title:v2,tags:v3,rating:v8};};
Enter fullscreen modeExit fullscreen mode

Thanks toeval, we can eliminate function calls and inline all type validations usingif statements. Also, knowing about theOutput type at runtime allows us to perform transformations with zero wasteful object allocations, optimizing the operation for JavaScript engines.

Interestingly, you probably think that callingeval itself is slow, and I thought this myself. However, it was actually not as slow as I expected. For example, creating a simple nested object schema and calling the parser once happened to be 1.8 times faster with ReScript Schema using eval than Zod. I really put a lot of effort into making it as fast as possible, and I have to thank theReScript language and the people behind it for allowing me to write very performant and safe code.

Talking aboutArkType, they use the same approach with eval and have similar potential to ReScript Schema, but their evaluated code is not there yet. Currently, their operations are a little bit slower, and the schema creation is significantly slower. But I can see that it can somewhat catch up in the future.

What other libraries will never be able to catch up on is the ability to reshape schema declaratively. And this is why I say that ReScript Schema is faster than Typia. Also, Typia doesn't always generate the most optimized code, e.g., for optional fields. And it doesn't come with many built-in operations specifically optimized for the desired use case. Still, this is an excellent library with Fast JSON Serialization and Protocol Buffer Encoding features, which I'm still yet to implement.

Ecosystem

When choosing a schema library for your project, where performance is not a concern, the ecosystem is the most important factor to consider. With a schema, you can do millions of things by knowing the type of representation in runtime. Such as JSON Schema generation, describing database schemas, optimized comparison and hashing, encoding to proto buff, building forms, mocking data, communicating with AI, and much more.

Zod is definitely a winner here. I counted 78 libraries integrating with Zod at the moment of writing the article. There are even some where you provide a Zod schema, and it renders a Vue page with a form prompting for the data. This is just too convenient for not using it for prototyping.

But if you don't need something super specific, ReScript Schema has a decent ecosystem itself, which is comparable to Valibot and ArkType. Actually, it has an even higher potential thanks to the ability to adjust Shape and automatically Reverse the schema. A good example of this isReScript Rest, which combines the DX oftRPC while staying unopinionated likets-rest. I also built many powerful tools around ReScript Schema, but I have to admit that I haven't added TS support yet. Let me know if you find something interesting to use, and I'll do this asap 😁

Also, ReScript Schema supportsStandard Schema, a common interface for TypeScript validation libraries. It was recently designed by the creators of Zod, Valibot, and ArkType and has already been integrated into many popular libraries. This means that you can use ReScript Schema withtRPC,TanStack Form,TanStack Router,Hono, and 19+ more at the time of writing the article.

Conclusion

As the title says, I wholeheartedly believe that ReScript Schema is the future of schema libraries. It offers both DX, performance, bundle size, and many innovative features. I tried to cover all of them at a high level, and I hope I managed to make you at least a little bit interested 👌

I don't persuade you to choose ReScript Schema for your next project, and I actually still recommend Zod when somebody asks me. But I'll definitely appreciate astar andX follow 🙏

Let's see how the future of schema libraries will turn out. Maybe I'll rename ReScript Schema to something dope and become more popular than Zod? Cheers 😁

Top comments(16)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
1ce profile image
Sby
I write good code. Interested in moving to another country.
  • Joined

Nice work! Definitely will consider it next time I need a parser or a validator.
By the way, since you're familiar with Typia, have you by any chance seen a recent post by Typia's author about its performance with the Bun runtime being 20 times slower?
Would you perhaps have any comments on that? I'm also interested in how ReScript Schema performs with different runtimes (Node, Deno, Bun, browser)

CollapseExpand
 
dzakh profile image
Dmitry Zakharov
Build fastest tools with best DX 🫡
  • Location
    Georgia, Batumi
  • Work
    Envio
  • Joined

Interesting, this is a new one. Just read it and left a likedev.to/samchon/bun-is-up-to-20x-sl...

Node.js, Deno and Browser are fine; the only problem is with Bun, which you can see in the community benchmarkmoltar.github.io/typescript-runtim...

I can see many comments in the article where people claim that it's a Typia specific problem, but Typia is a codegen library, and it doesn't do anything special in runtime. This means if you write a function like(input) => "string" === typeof input by hand and run it multiple times, it'll execute multiple times slower in Bun than in any other runtime.

CollapseExpand
 
retakenroots profile image
Rene Kootstra
Author of enge-js a pure JavaScript playstation 1 emulator and professional developer for 25+ years.
  • Joined

Hmm very interesting article. Though when i read eval it raised some concerns. Nonetheless good job.

CollapseExpand
 
dzakh profile image
Dmitry Zakharov
Build fastest tools with best DX 🫡
  • Location
    Georgia, Batumi
  • Work
    Envio
  • Joined

It's indeed good to be aware of this because if you build for serverless or some widget that is embedded as a CDN to someone else website (CSP) there might be problems. But as I said, this is ~1% of use cases.

Also, ArkType uses eval, and according toColin's words, he plans to add Eval mode to Zod v4.

Ideally, there should be a fallback mode to be able to work without Eval when it's not supported. I actually recently got an idea of how to implement it without reducing the quality of the library. But it'll probably take several months for me to implement.

CollapseExpand
 
dhruvgarg79 profile image
Dhruv garg
  • Location
    Bengaluru, India
  • Education
    B. Tech in computer science
  • Work
    Tech lead
  • Joined

what about comparison with typebox? It's also much faster than zod.

CollapseExpand
 
dzakh profile image
Dmitry Zakharov
Build fastest tools with best DX 🫡
  • Location
    Georgia, Batumi
  • Work
    Envio
  • Joined

This is actually very good, and it's very mature. I actually had a misunderstanding about it being a worse version of Typia, but after double-checking the docs, I was really impressed by it.

In the benchmark, you can indeed see that it's fastmoltar.github.io/typescript-runtim...

But there are some trade-offs that are solved in ReScript Schema:

  • Optimised check only supports validation, not parsing with data transformations
  • I find the DX of ReScript Schema to be more friendly for web developers. Also, reshaping and reversing are still unbeaten
  • The TypeBox packages size is huge, making it not the best fit for web developmentgithub.com/sinclairzx81/typebox?ta...

So, at the moment of writing the article I think ReScript Schema is a better library if you use it for Web, but for server-side TypeBox is more mature and provides more flexibility and features. Although there are still some features of ReScript Schema you might want to use, which are not a part of TypeBox 😉

ReScript Schema v10 is coming, which will improve the flexibility similar to TypeBox while boosting DX even more, exceeding Zod and ArkType levels 😁

CollapseExpand
 
dzakh profile image
Dmitry Zakharov
Build fastest tools with best DX 🫡
  • Location
    Georgia, Batumi
  • Work
    Envio
  • Joined

Ok, I decided to double-check the package size table from their docs and it actually happened that the package size is not bigbundlephobia.com/package/@sinclair...

Another thing to compare is the much more readable error messages by default in ReScript Schema.

Thread Thread
 
dhruvgarg79 profile image
Dhruv garg
  • Location
    Bengaluru, India
  • Education
    B. Tech in computer science
  • Work
    Tech lead
  • Joined

I will give it a try in near future, thanks for amazing library my friend :)

CollapseExpand
 
alexdev404 profile image
Immanuel Garcia
  • Joined

So why even use this if you can just use Zod or Valibot, bypassing any or all of thateval magic you did just for a few extra kilobytes?

CollapseExpand
 
dzakh profile image
Dmitry Zakharov
Build fastest tools with best DX 🫡
  • Location
    Georgia, Batumi
  • Work
    Envio
  • Joined
• Edited on• Edited

Eval actually makes the size bigger 😅

There are some unique features you can't find in any other library together:

  • Nice API with good DX
  • Top performance
  • Ability to conveniently and efficiently transform data when you parse
  • Ability to transform data without validation
  • Ability to get schema for the output type
  • Flexible set of operations

If you don't need it, then use Zod or Valibot. Both of them are good libraries I like.

CollapseExpand
 
jpeggdev profile image
Jeff Pegg
  • Location
    Tulsa, Oklahoma
  • Joined

Hi Dmitry, nice article. In your last code example showing the under the hood code, on the 3rd line it says e7, where does the capital I come from or is that a typo?

CollapseExpand
 
dzakh profile image
Dmitry Zakharov
Build fastest tools with best DX 🫡
  • Location
    Georgia, Batumi
  • Work
    Envio
  • Joined

Hm, this looks like a copy-paste bug from Google Docs where I initially written the article. It should be a lower case i. As fore it comes from the function context and used for safe embedding to eval code.

CollapseExpand
 
jpeggdev profile image
Jeff Pegg
  • Location
    Tulsa, Oklahoma
  • Joined

I didn't even notice it stripped the brackets and parenthesis from my comment.
e[7](I)

CollapseExpand
 
ravi-coding profile image
Ravindra Kumar
Full-Stack Developer | MERN & Python | Passionate about building web apps and OpenAi APIs. Always learning and sharing knowledge with the community. Let's connect and create something awesome! 🚀
  • Email
  • Location
    New Delhi
  • Education
    2016
  • Pronouns
    He/him
  • Work
    Full-Stack Developer | MERN & Django Specialist | Currently enhancing skills in AWS and cloud tech
  • Joined

Awesome !

CollapseExpand
 
jesterly profile image
jesterly
  • Joined

Very cool, and thanks for the heads up about eval. It's a shame we can't use this in browser extensions because eval is not allowed in MV3 :-(.

CollapseExpand
 
dzakh profile image
Dmitry Zakharov
Build fastest tools with best DX 🫡
  • Location
    Georgia, Batumi
  • Work
    Envio
  • Joined

Finishing V10 with some fantastic improvements. I'm ready to take over the world 😁

Image description

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Build fastest tools with best DX 🫡
  • Location
    Georgia, Batumi
  • Work
    Envio
  • Joined

More fromDmitry Zakharov

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp