Schema validation with static type inference
flowchart TD
Schema -->|codecFor| Codec
Schema -->|guardFor| Guard
Schema -->|arbitraryFor| Arbitrary
Schema -->|prettyFor| Pretty
Features
- deriving single artifacts from a
Schema
:
Codec
(all in one artifact)- custom artifact compilers
- custom
Schema
combinators - custom data types
- custom decode errors (TODO)
- refinements (TODO)
- versioning (TODO)
- migration (TODO)
Summary
Schema definition
import * as C from "@fp-ts/schema/Codec";
const Person = C.struct({
name: C.string,
age: C.number,
});
Extract the inferred type
type Person = C.Infer<typeof Person>;
Decode from unknown
import * as DE from "@fp-ts/schema/DecodeError";
const unknown: unknown = { name: "name", age: 18 };
expect(Person.decode(unknown)).toEqual(C.success({ name: "name", age: 18 }));
expect(Person.decode(null)).toEqual(
C.failure(DE.notType(Symbol.for("@fp-ts/schema/data/UnknownObject"), null))
);
Parse from JSON
string
expect(() => Person.parseOrThrow("malformed")).toThrow(
new Error("Cannot parse JSON from: malformed")
);
expect(() => Person.parseOrThrow("{}")).toThrow(
new Error("Cannot decode JSON")
);
expect(() =>
Person.parseOrThrow("{}", (errors) => JSON.stringify(errors))
).toThrow(
new Error(
'Cannot decode JSON, errors: [{"_tag":"Key","key":"name","errors":[{"_tag":"NotType"}]}]'
)
);
Encode to unknown
expect(Person.encode({ name: "name", age: 18 })).toEqual({
name: "name",
age: 18,
});
Encode to JSON
string
expect(Person.stringify({ name: "name", age: 18 })).toEqual(
'{"name":"name","age":18}'
);
Guard
expect(Person.is({ name: "name", age: 18 })).toEqual(true);
expect(Person.is(null)).toEqual(false);
Pretty print
expect(Person.pretty({ name: "name", age: 18 })).toEqual(
'{ "name": "name", "age": 18 }'
);
fast-check
Arbitrary
import * as fc from "fast-check";
console.log(fc.sample(Person.arbitrary(fc), 2));
Custom artifact compilers
src/Pretty.ts
, src/Guard.ts
and src/Arbitrary.ts
are good examples of defining a custom compiler.
Custom schema combinators
Examples in /src/Schema.ts
.
All the combinators defined in /src/Schema.ts
could be implemented in userland.
Custom data types
Examples in /src/data/*
Understanding Schemas
A schema is a description of a data structure that can be used to generate various artifacts from a single declaration.
From a technical point of view a schema is just a typed wrapper of an AST
value
interface Schema<in out A> {
readonly ast: AST;
}
The AST
type represents a tiny portion of the TypeScript AST, roughly speaking the part describing ADTs (algebraic data types),
i.e. products (like structs and tuples) and unions.
This means that you can define your own schema constructors / combinators as long as you are able to manipulate the AST
type accordingly, let's see an example.
Say we want to define a pair
schema constructor, which takes a Schema<A>
as input and returns a Schema<readonly [A, A]>
as output.
First of all we need to define the signature of pair
import * as S from "@fp-ts/schema/Schema";
declare const pair: <A>(schema: S.Schema<A>) => S.Schema<readonly [A, A]>;
Then we can implement the body using the APIs exported by the AST.ts
module
import * as AST from "@fp-ts/schema/AST";
import * as O from "@fp-ts/data/Option";
const pair = <A>(schema: S.Schema<A>): S.Schema<readonly [A, A]> => {
const item = AST.component(
schema.ast,
false
);
const tuple = AST.tuple(
[item, item],
O.none,
true
);
return S.make(tuple);
};
The goal of this example was showing the low-level APIs of the AST
module, but the same result can
be achieved using the much more handy APIs of the Schema
module
const pair = <A>(schema: S.Schema<A>): S.Schema<readonly [A, A]> =>
S.tuple(schema, schema);
Please note that the S.tuple
itself is nothing special and can be defined in userland
export const tuple = <Components extends ReadonlyArray<Schema<any>>>(
...components: Components
): Schema<{ readonly [K in keyof Components]: Infer<Components[K]> }> =>
make(
AST.tuple(
components.map((c) => AST.component(c.ast, false)),
O.none,
true
)
);
Now you can compile your pair
schemas using the codecFor
compiler
import * as C from "@fp-ts/schema/Codec";
const myNumberPair = C.codecFor(pair(S.number));
expect(myNumberPair.is([1, 2])).toEqual(true);
expect(myNumberPair.is([1, "a"])).toEqual(false);
Guard
A Guard
is a derivable artifact that is able to refine a value of type unknown
to a value of type A
.
interface Guard<in out A> extends Schema<A> {
readonly is: (input: unknown) => input is A;
}
Arbitrary
An Arbitrary
is a derivable artifact that is able to produce fast-check
arbitraries.
interface Arbitrary<in out A> extends Schema<A> {
readonly arbitrary: (fc: typeof FastCheck) => FastCheck.Arbitrary<A>;
}
Pretty
A Pretty
is a derivable artifact that is able to pretty print a value of type A
.
interface Pretty<in out A> extends Schema<A> {
readonly pretty: (a: A) => string;
}
Codec
A Codec
is a derivable artifact that is able to:
- decode a value of type
unknown
to a value of type A
- encode a value of type
A
to a value of type unknown
A Codec
is also a Guard
, an Arbitrary
and a Pretty
.
interface Codec<in out A>
extends Schema<A>,
Decoder<unknown, A>,
Encoder<unknown, A>,
Guard<A>,
Arbitrary<A>,
Pretty<A> {}
Basic usage
Primitives
import * as C from "@fp-ts/schema/Codec";
C.string;
C.number;
C.boolean;
C.bigint;
C.symbol;
C.unknown;
C.any;
Filters
Note. Filters don't change the Schema
type.
pipe(C.string, C.minLength(1));
pipe(C.string, C.maxLength(10));
pipe(C.number, C.lessThan(0));
pipe(C.number, C.lessThanOrEqualTo(0));
pipe(C.number, C.greaterThan(10));
pipe(C.number, C.greaterThanOrEqualTo(10));
pipe(C.number, C.int);
Literals
C.literal("a");
C.literal("a", "b", "c");
Native enums
enum Fruits {
Apple,
Banana,
}
C.nativeEnum(Fruits);
Unions
C.union(C.string, C.number);
Tuples
C.tuple(C.string, C.number);
Rest element
pipe(C.tuple(C.string, C.number), C.restElement(C.boolean));
Arrays
C.array(C.number);
Non empty arrays
Equivalent to pipe(tuple(item), restElement(item))
C.nonEmptyArray(C.number);
Structs
C.struct({ a: C.string, b: C.number });
Optional fields
C.struct({ a: C.string, b: C.number }, { c: C.boolean });
Pick
pipe(C.struct({ a: C.string, b: C.number }), C.pick("a"));
Omit
pipe(C.struct({ a: C.string, b: C.number }), C.omit("a"));
Partial
C.partial(C.struct({ a: C.string, b: C.number }));
String index signature
C.stringIndexSignature(C.string);
Symbol index signature
C.symbolIndexSignature(C.string);
Extend
pipe(
C.struct({ a: C.string, b: C.string }),
C.extend(C.stringIndexSignature(C.string))
);
Option
C.option(C.number);
ReadonlySet
C.readonlySet(C.number);
Chunk
C.chunk(C.number);
List
C.list(C.number);
Documentation
License
The MIT License (MIT)