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"])})
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";}>
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>
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))
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.2 | Zod@3.24.1 | Valibot@1.0.0-beta.14 | ArkType@2.0.4 | |
---|---|---|---|---|
Total size (minified + gzipped) | 12.7 kB | 15.2 kB | 12.3 kB | 40.8 kB |
Example size (minified + gzipped) | 5.14 kB | 14.5 kB | 1.39 kB | 40.7 kB |
Playground | Link | Link | Link | Link |
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,}
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]))})
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)}
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" }
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],}));
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"] }
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"]],};};
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"
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!");
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};};
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)

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)

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.

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

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.

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

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 😁

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.

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

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?

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.

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.

- Email
- LocationNew Delhi
- Education2016
- PronounsHe/him
- WorkFull-Stack Developer | MERN & Django Specialist | Currently enhancing skills in AWS and cloud tech
- Joined
Awesome !
For further actions, you may consider blocking this person and/orreporting abuse