4 ms·
A lightweight TypeScript library for assertion-based runtime data validation
- nayajunimesh 1y agoMost validation libraries like Zod create deep clones of your data during validation, which can impact performance in high-throughput applications. I built decode-kit to take a different approach: assertion-based validation that validates and narrows TypeScript types in-place, without any copying or transformation. Here's what the API looks like in practice: import { object, string, number, validate } from "decode-kit"; // Example of untrusted data (e.g., from an API) const input: unknown = { id: 123, name: "Alice" }; // Validate the data (throws if validation fails) validate(input, object({ id: number(), name: string() })); // `input` is now typed as { id: number; name: string } console.log(input.id, input.name); When validation fails, decode-kit takes an equally thoughtful approach. Rather than being prescriptive about error formatting, it exposes a structured error system with an AST-like path that precisely indicates where validation failed. It does include a sensible default error message for debugging, but you can also traverse the error path to build whatever error handling approach fits your application - from simple logging to sophisticated user-facing messages. The library also follows a fail-fast approach, immediately throwing when validation fails, which provides both better performance and clearer error messages by focusing on the first issue encountered. I'd love to hear your thoughts and feedback on this approach.
- deleted 1y ago[deleted]
- kenward 1y ago> fail-fast approach, immediately throwing when validation fails would this mask any errors that would occur later in the validation?
- nayajunimesh 1y agoWith the fail-fast approach, yes - unless we introduce an option to collect all errors. In my own applications, I have found this to be a better default because the 'average' requests is valid and paying a constant overhead just to be thorough on rare invalid cases can be wasteful. My overall takeaway has mostly been to not optimize for the worst case by default. Keep fail-fast as baseline for boundaries and hot paths, and selectively enable “collect all” where it demonstrably saves human time.
- yencabulator 1y agoA useful counterexample, where one wants to see all errors, is validating form input. But it's very much ok to say your library is not meant for that use case!
- nayajunimesh 1y agoThat is a good point! How do you feel about fail-fast by default but being able to configure a validator to collect all errors (like Zod does by default)? Something like array(number(), { failFast: false }); We currently expose error as a tree structure so that it's easy to map an error to a value or build custom error messages (we only provide debug error message) and I haven't been able to come up with a satisfactory error API that accommodates multiple error paths, but you raise an excellent point. Thanks for pointing out.
- yencabulator 1y agoMy instinct is to say keep it small, simple & focused.
- scottmas 1y agoThe whole benefit over zod seems to be perf, so could you do some benchmarking? I wonder if it’s worth it
- nayajunimesh 1y agoHey, I did some benchmarks if you're interested - benchmarks results are in the README.md. https://github.com/nimeshnayaju/zod https://github.com/nimeshnayaju/zod (Fork of Zod's repo which already included benchmarks comparing Zod 4 against Zod 3, so I simply integrated my validation library) https://github.com/nimeshnayaju/valibot-benchmarks https://github.com/nimeshnayaju/valibot-benchmarks (An unofficial benchmark suggested in another comment comparing Valibot against Zod)
- nayajunimesh 1y agoYes, the primary focus is memory efficiency; performance improvement is a side effect of that. From my own benchmarks, I have found that to be the case. If you're validating thousands of objects per second or working with memory constraints, the difference becomes quite significant. Happy to share the full benchmark code if you'd like to run them yourself!
- deleted 1y ago[deleted]
- typeofhuman 1y agoYou should include this benchmark in your repo and README if you want to build trust. I think anything that declares itself as a performance improvement over the competition ought to prove it!
- revskill 1y agoLove it. I hate zod.
- jasonlernerman 1y agoarktype does this too!
- matt-attack 1y agoArktype is fantastic.
- seabass 1y agoI was surprised by the source including a bunch of try/catch, which results in deopts for that code path as far as I understand, given that the stated benefit over Zod and other validators was that this should be run in performance critical code. I’d be curious to see benchmarks that show whether this is faster than zod, valibot, and zod4 mini in hot code paths.
- CharlesW 1y agoOP, you could potentially leverage an existing benchmark suite: https://naruaway.github.io/valibot-benchmarks/ https://naruaway.github.io/valibot-benchmarks/
- nayajunimesh 1y agoHey, thanks for the link! I was able to add the library to the benchmark quite easily. Here's the fork if you're interested: https://github.com/nimeshnayaju/valibot-benchmarks https://github.com/nimeshnayaju/valibot-benchmarks I have included the results I obtained from running the benchmark in the README.md. I'd love for you to also take a look at the changed code to see if I may have missed something with the integration. Curious to hear your feedback too!
- CharlesW 1y agoNicely done, and thanks for following up!
- nayajunimesh 1y agoThank you, this is quite helpful. Will take a detailed look very soon!
- typeofhuman 1y agoHow do you know you're tool is more performant?
- 1y ago
- sizediterable 1y agoNeed it be said https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/ https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...
- ale 1y agoLibraries like these are meant for runtime validation. I agree though. I prefer to use the compiler itself (tsc --noEmit) than recreating the validation logic.
- mirekrusin 1y agoIt doesn't compete with static type system, it complements it. Static type system in typescript can't do anything with unknown/any values that are crossing i/o boundary - they require runtime assertion to bring them into statically typed, safer world.
- tomjakubowski 1y agoLibraries like runtypes, zod, et al. market themselves as validation libraries, but they function as parsing libraries in the sense this article means: with them you "parse" untyped POJOs at the I/O boundary and get typed values (or a raised exception) out the other end. Typescript language features like branded types, private constructors can make it so those values can only be constructed through the parse method. They're really not much different, in terms of type safety*, from something like Serde. *: they are of course different in other important ways -- like that Serde can flexibly work with all kinds of serialized formats.
- nayajunimesh 1y agoWe actually use the idea of branded types in one of the validators (iso8601), and I also understand that it doesn't replace fully transformed values. https://github.com/nimeshnayaju/valleys?tab=readme-ov-file#iso8601-1 https://github.com/nimeshnayaju/valleys?tab=readme-ov-file#i...
- nayajunimesh 1y ago
- orangee 1y agoassertion-based API is a neat concept
- cluckindan 1y agoThis would be way more powerful if there was a way to infer a validator from a TypeScript type.
- gr4vityWall 1y agoIt would be nice to provide some benchmarks comparing it to Zod, arktype, etc. Comparing it across different runtimes (Node.js, Bun, web browsers, etc.) would be great, too.
- nayajunimesh 1y agoHey, I did some benchmarks if you're interested - benchmarks results are in the README.md. https://github.com/nimeshnayaju/zod https://github.com/nimeshnayaju/zod (Fork of Zod's repo which already included benchmarks comparing Zod 4 against Zod 3, so I simply integrated my validation library) https://github.com/nimeshnayaju/valibot-benchmarks https://github.com/nimeshnayaju/valibot-benchmarks (An unofficial benchmark from another user comparing Valibot against Zod)
- gr4vityWall 1y agoThanks, I appreciate that you added them. :) They show a lot of potential and promising results. Any guess why Valley is slower for parsing objects with primitive values?