What is superstruct?
The superstruct npm package is a library for validating, coercing, and structuring data in JavaScript and TypeScript. It allows developers to define interfaces and run-time type checks for JavaScript data structures, ensuring that data conforms to specified schemas.
What are superstruct's main functionalities?
Validation
Superstruct can be used to validate data against a defined schema. The example shows how to define a User struct and validate an object against it.
{"User": "const User = struct({name: 'string', age: 'number'}); const data = {name: 'Alice', age: 25}; const [error, user] = User.validate(data); if (error) { throw error; }"}
Coercion
Superstruct can coerce data to match a schema by applying default values. In the example, a UserWithDefaults struct is defined with an optional isAdmin field that defaults to false.
{"UserWithDefaults": "const UserWithDefaults = struct({name: 'string', age: 'number', isAdmin: 'boolean?'}); const data = {name: 'Bob', age: 30}; const user = UserWithDefaults.create(data);"}
Partial Structs
Superstruct allows creating partial structs, which can validate data that may not include all fields of the struct. The example demonstrates how to define a partial struct based on the User struct.
{"PartialUser": "const User = struct({name: 'string', age: 'number'}); const PartialUser = struct.partial(User); const data = {name: 'Charlie'}; const [error, partialUser] = PartialUser.validate(data); if (error) { throw error; }"}
Dynamic Structs
Superstruct can define dynamic structs that change based on the input data. The example shows a DynamicUser struct that requires a permissions array only if the isAdmin property is true.
{"DynamicUser": "const DynamicUser = struct.dynamic((value, branch, path) => { if (value && value.isAdmin) { return struct({name: 'string', age: 'number', permissions: 'array'}); } else { return struct({name: 'string', age: 'number'}); } }); const data = {name: 'Dave', age: 40, isAdmin: true, permissions: ['read', 'write']}; const [error, dynamicUser] = DynamicUser.validate(data); if (error) { throw error; }"}
Other packages similar to superstruct
joi
Joi is a powerful schema description language and data validator for JavaScript. It offers a similar API to superstruct but with a more extensive set of features for describing and validating data structures, including custom validation functions.
yup
Yup is a JavaScript schema builder for value parsing and validation. It defines a schema with an expressive API and is often used with form libraries like Formik. Yup schemas are immutable and composable, and it provides a slightly different API compared to superstruct.
ajv
Ajv is a JSON schema validator that supports draft-06/07/2019-09 JSON Schema standards. It is known for its performance and is used to validate JSON data on the server-side and in the browser. Unlike superstruct, Ajv relies on JSON Schema, which is a declarative language for validating the structure of JSON data.
A simple and composable way
to validate data in Javascript.
Example •
Why? •
Principles •
Documentation
Superstruct makes it easy to define interfaces and then validate Javascript data against them. Its type annotation API was inspired by Typescript, Flow and GraphQL. But Superstruct is designed for runtime data validations, for example when accepting input in a REST or GraphQL API.
Example
Superstruct exports a struct
factory for creating functions that validate data against a specific schema:
import struct from 'superstruct'
const validate = struct({
id: 'number',
title: 'string',
is_published: 'boolean',
tags: ['string'],
author: {
id: 'number',
}
})
const data = {
id: 34,
title: 'Hello World',
is_published: true,
tags: ['announcements'],
author: {
id: 1,
}
}
validate(data)
The schema definition syntax was inspired by Typescript, Flow and GraphQL.
But you can also define your own types—specific to your application's requirements—by using the exported superstruct
function. For example:
import { superstruct } from 'superstruct'
import isUuid from 'is-uuid'
import isEmail from 'is-email'
const struct = superstruct({
types: {
uuid: v => isUuid.v5(v),
email: v => isEmail(v) && v.length < 256,
}
})
const validate = struct({
id: 'uuid',
email: 'email',
is_admin: 'boolean?',
})
const data = {
id: '5a2de30a-a736-5aea-8f7f-ad0f019cdc00',
email: 'jane@example.com',
}
validate(data)
Why?
There are lots of existing validation libraries. Some of them, like Joi, express-validator
, validator.js
or ajv
are decently popular. But all of them exhibit many issues that lead to hard to maintain codebases...
-
They don't throw errors. Many validators simply return true/false
or return string error messages. This was helpful in the days of callbacks, when using throw
was discouraged, but in modern Javascript using throw
leads to much simpler and terser code.
-
They don't expose useful error information. For the validators that do throw
, they often throw over-simplified, message-only errors without any extra details about the reason the error occurred. This makes it very difficult to customize the errors to make them helpful for end users.
-
They aren't designed around custom types. Many validators ship with built-in types like emails, URLs, UUIDs, etc. with no easy way to know how they're implemented. And when defining your own custom types—which any reasonably sized codebase needs to do—the APIs are needlessly complex and hard to re-use.
-
They don't encourage single sources of truth. Many existing APIs encourage re-defining custom data type requirements like maximum lengths, custom types, error messages, etc. over and over, with the source of truth being spread out across many files, which makes consistentcy difficult to maintain.
-
They don't compile schemas for performance. Some validators allow you to define schemas as plain Javascript objects, which seems nice at first. But it also means that they delegate the complex parsing of the schema logic to validation time, instead of doing the work up front for performance.
-
They are tightly coupled to other concerns. Many validators are implemented as plugins for Express or other HTTP frameworks, which is completely unnecessary and confusing to reason about. And since you can only use them with a web server you end up with even more fragmentation in your codebase.
Of course, not every validation library suffers from all of these issues, but most of them exhibit at least one. If you've run into this problem before, you might like Superstruct.
Which brings me to how Superstruct solves these issues...
Principles
-
Customizable types. Superstruct's power is in making it easy to define an entire set of custom data types that are specific to your application, and defined in a single place, so you have full control over your requirements.
-
Unopinionated defaults. Superscript ships with native Javascript types, and everything else is customizable, so you never have to fight to override decisions made by "core" that differ from your application's needs.
-
Composable interfaces. Superstruct interfaces are composable, so you can break down commonly-repeated pieces of data into components, and compose them to build up the more complex objects.
-
Terse schemas. The schemas in Superstruct are designed to be extremely terse. This makes them very easy to read and write, encouraging you to have full data validation coverage.
-
Compiled validators. Superstruct does the work of compiling its schemas up front, so that it doesn't have to spend lots of time performing expensive tasks for every call to the validation functions in your hot code paths.
-
Useful errors. The errors that Superstruct throws contain all the information you need to convert them into your own application-specific errors easy, which means more helpful errors for your end users!
-
Familiar API. The Superstruct API was heavily inspired by Typescript, Flow and GraphQL. If you're familiar with any of those, then its schema definition API will feel very natural to use, so you can get started quickly.
Documentation
License
This package is MIT-licensed.