Security News
cURL Project and Go Security Teams Reject CVSS as Broken
cURL and Go security teams are publicly rejecting CVSS as flawed for assessing vulnerabilities and are calling for more accurate, context-aware approaches.
JSON Schema inspired library for validation, decoding and type conversion
JSON Schema inspired library for validating and decoding messages.
Status: beta
JSON Schema is great, but sometimes it can be a bit too much.
It has a lot of features. Some of the features are seldomly used in practice, but still contribute to the size of standards-compliant libraries. More importantly, combinations of different features raise the complexity exponentially, resulting in non-obvious behaviour and, ultimately, bugs in production.
It's too flexible. You can write definitions, references, mix in different references and keywords with combinators, implement mutually recursive schemas, etc, etc. Allowing too much means limiting the capabilities of how schemas can be consumed for the purposes other than validation (e.g. good luck generating a reasonable documentation for a yarn ball of $oneOf
, $anyOf
and $ref
).
It has an Achilles' heel: {}
is a perfectly valid schema which does no validation whatsoever. If you do not validates schemas themselves, then they are prone to errors like { tpye: 'number' }
which does no validation. This makes TypeScript users cringe because it means any
-as-a-fallback.
Type conversions and coercions are not currently part of standards. This makes it tricky to use for the purposes like message encoding/decoding (i.e. similar to Protobuf) where schema evolution should be taken in consideration.
Some libraries offer some extras (defaults, coercion, etc) but tend to perform modifications in-place. We think this is a signficant downside to API ergonomics and opted to never apply in-place modifications.
// Type definitions for compile-time checks
interface Product {
title: string;
price: Price;
salePrice?: Price;
promoCode: string | null;
tags: string[];
}
interface Price {
value: number;
currency: 'gbp' | 'eur' | 'usd';
}
// Schema performs the checks in runtime so it needs a runtime spec.
// TypeScript makes sure that the runtime spec matches compile-time types
// (including all nested types and designations like `optional`, `nullable`, etc).
const PriceSchema: SchemaDef<Price> = {
type: 'object',
properties: {
value: { type: 'integer' },
currency: {
type: 'string',
enum: ['gbp', 'eur', 'usd'],
default: 'gbp',
},
},
};
const ProductSchema: SchemaDef<Product> = {
type: 'object',
properties: {
title: {
type: 'string',
default: 'Unknown Product',
},
price: PriceSchema,
salePrice: { ...PriceSchema, optional: true },
promoCode: {
type: 'string',
nullable: true,
},
tags: {
type: 'array',
items: {
type: 'string',
}
},
}
};
// Schema API
// Create an object with defaults (argument is DeepPartial<T>):
const o1 = ProductSchema.create({});
assert.deepStrictEqual(o1, {
title: 'Unknown Product',
price: { value: 0, currency: 'gbp' },
promoCode: null,
tags: [],
});
// Decode an object (argument is unknown):
const o2 = ProductSchema.decode({
title: 'Shampoo',
price: { value: '42' },
});
assert.deepStrictEqual(o1, {
title: 'Shampoo',
price: { value: 42, currency: 'gbp' },
promoCode: null,
tags: [],
});
// Decode throws if validation fails
try {
ProductSchema.decode({
title: { something: 'wrong' },
});
} catch (error) {
// ValidationError: object validation failed:
// - .title must be a string
}
Create a schema with new Schema<T>({ ... })
where T
is either a type
or an interface
. TypeScript will guide you through composing a schema definition that matches T
.
schema.decode(value: unknown)
decodes the message as follows:
null
or undefined
is returned according to schematype: 'any'
schema is returned as-istype: 'ref'
schema is dereferenced and validatedschema.create(value: DeepPartial<T>)
is a convenience method that offers an additional advantage of type-checking value
— useful for creating objects programmatically from code so that the arguments you pass can be statically checked.
Airtight supports exactly 8 types with following validators:
type: 'boolean'
type: 'number'
minimum?: number
maximum?: number
type: 'integer'
minimum?: number
maximum?: number
type: 'string'
regex?: string
regexFlags?: string
enum?: string[]
type: 'object'
properties: ObjectPropsDef<T>
additionalProperties?: SchemaDef<any>
type: 'array'
type: 'array'
items: SchemaDef<T>
type: 'any'
means no validation and has to be explicittype: 'ref'
(see Recursive schema)Additionally, all schema definitions support following fields:
id?: string
— a string identifier used in referencing and documentationtitle?: string
description?: string
metadata?: any
— arbitrary data attached to schemadefault?: T | (() => T)
— used by decode
in non-throw mode when validation failsrefs?: SchemaRefs
— a collection of auxiliary schema that can be used in type: 'ref'
Type conversion happens automatically when the actual type does not match the desired type according to the following rules:
Schema type | Type (Value) | Result |
---|---|---|
boolean | number (<= 0) | false |
boolean | number (> 0) | true |
boolean | number (any other) | ❌ |
boolean | string ("true") | true |
boolean | string ("false") | false |
boolean | string (any other) | ❌ |
number | string ("42") | 42 |
number | boolean (true) | 1 |
number | boolean (false) | 0 |
integer | string ("42") | 42 |
integer | string ("42.234") | 42 |
integer | boolean (true) | 1 |
integer | boolean (false) | 0 |
integer | number (42.234) | 42 |
string | null | '' |
string | number (42.234) | '42.243' |
string | boolean (true) | 'true' |
string | boolean (false) | 'false' |
array | null | ❌ |
array | any (value) | [value] |
any | array of length 1 | convert(array[0]) |
Summary:
null
As mentioned previously, type conversion does not count as validation error.
A failed type conversion means that the default value is used if throw: false
or the error is thrown if throw: true
.
It is possible to create a schema with mutual recursion like this:
interface Foo { bar?: Bar }
interface Bar { foo?: Foo }
const refs: SchemaRefs = [
{
id: 'Foo',
type: 'object',
properties: {
bar: { type: 'ref', schemaId: 'Bar', optional: true, },
}
},
{
id: 'Bar',
type: 'object',
properties: {
foo: { type: 'ref', schemaId: 'Foo', optional: true, },
}
}
];
const FooSchema = new Schema<Foo>({
type: 'ref',
schemaId: 'Foo',
refs,
});
const BarSchema = new Schema<Foo>({
type: 'ref',
schemaId: 'Bar',
refs,
});
assert.deepStrictEqual(
FooSchema.decode({ bar: { foo: { bar: {} } } }),
{ bar: { foo: { bar: {} } } });
assert.deepStrictEqual(
BarSchema.decode({ foo: { bar: {} } }),
{ foo: { bar: {} } });
FAQs
JSON Schema inspired library for validation, decoding and type conversion
We found that airtight demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
cURL and Go security teams are publicly rejecting CVSS as flawed for assessing vulnerabilities and are calling for more accurate, context-aware approaches.
Security News
Bun 1.2 enhances its JavaScript runtime with 90% Node.js compatibility, built-in S3 and Postgres support, HTML Imports, and faster, cloud-first performance.
Security News
Biden's executive order pushes for AI-driven cybersecurity, software supply chain transparency, and stronger protections for federal and open source systems.