@serafin/schema-builder
v0.18.15
Published
A Typescript JSON Schema library that creates a schema and its type at the same time
Downloads
22,305
Readme
Serafin Schema Builder is a library that ease the creation of a JSON Schema and its associated Typescript type.
Installation
npm i @serafin/schema-builder
Why Schema Builder?
JSON schema is the base of Open API so it's really important for Serafin framework. JSON Schema is powerful but it is also verbose.
On top of a JSON schema, you also have to create the Typescript interface that it represents. If you take in account other schemas and interfaces you have to define (one for the post body, one for the patch body, one for get query parameters, etc.), it starts to be problematic.
Schema builder is here to save you from all this tedious work!
To summarize, this library allows you to programmatically create a JSON Schema and its associated typescript type at the same time.
A quick example
Let's create simple User and Task schemas.
// Schema for the Task
let taskSchema = SB.objectSchema(
{
title: "Task",
},
{
name: SB.stringSchema(),
progress: SB.numberSchema(),
isCompleted: [SB.booleanSchema(), undefined],
},
)
// Schema for the User
let userSchema = SB.objectSchema(
{
title: "User",
},
{
id: SB.stringSchema({ pattern: "\\w" }),
firstName: SB.stringSchema(),
lastName: SB.stringSchema(),
role: SB.enumSchema(["admin", "user"]),
email: SB.stringSchema({ format: "email" }),
tags: SB.arraySchema(SB.stringSchema(), { minItems: 1 }),
age: [SB.integerSchema(), undefined],
friendsIds: [SB.arraySchema(SB.stringSchema()), undefined],
tasks: SB.arraySchema(taskSchema),
},
)
// References to generated interfaces
type Task = typeof taskSchema.T
type User = typeof userSchema.T
With the code above, we have created two JSON schemas. You can access them with .schema
. userSchema.schema
for example contains :
{
"type": "object",
"additionalProperties": false,
"properties": {
"id": {
"pattern": "\\w",
"type": "string"
},
"firstName": {
"type": "string"
},
"lastName": {
"type": "string"
},
"role": {
"type": "string",
"enum": ["admin", "user"]
},
"email": {
"format": "email",
"type": "string"
},
"tags": {
"minItems": 1,
"type": "array",
"items": {
"type": "string"
}
},
"age": {
"type": "integer"
},
"tasks": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"type": "string"
},
"progress": {
"type": "number"
},
"isCompleted": {
"type": "boolean"
}
},
"required": ["name", "progress"]
}
}
},
"required": ["id", "firstName", "lastName", "role", "email", "tags", "tasks"]
}
Thanks to the power of type operations in Typescript, we also have created two interfaces. We can create an explicit reference to it using the typeof
keyword.
type User
is equivalent to the following :
type User = {
id: string
firstName: string
lastName: string
role: "admin" | "user"
email: string
tags: string[]
age?: number
tasks: {
name: string
progress: number
isCompleted?: boolean
}[]
}
We want also to have an alternative schema for the user when we send a patch request. Let's modify the initial schema:
let userPatchSchema = userSchema.pickProperties(["firstName", "lastName", "email", "age", "tags"]).toOptionals()
type UserPatch = typeof userPatchSchema.T
type UserPatch
is equivalent to the following :
type UserPatch = {
firstName?: string
lastName?: string
email?: string
tags?: string[]
age?: number
}
We can now use the validate
method to validate data against our schema. The validation use Ajv
with Json Schema draft #7 by default.
userPatchSchema.validate({
firstName: "John",
age: 42,
})
There's more! This was a simple example. This library provides also a lot of transformation operations that you can apply to your schemas.
Usage
Since it's a Typescript library, intellisense and code comments provide already a good description of the methods.
This section will focus on advanced transformation methods and how to use them. Refer to the code for the rest.
allOf, anyOf, oneOf, not
SchemaBuilder
contains static method to create allOf
, anyOf
, oneOf
and not
.
When you start using one of those in a SchemaBuilder
, most of the transformation methods won't work anymore. It's because they expect the schema to contains only properties
.
renameProperty
renameProperty
allows you to change the name of property without affecting its schema.
let schema = SchemaBuilder.emptySchema().addString("prop1").renameProperty("prop1", "prop2")
addAdditionalProperties
You can set additionalProperties
in your json schema using this method.
// additionalProperties is set to true and an any index signature is added to the generic interface
let schema = SchemaBuilder.emptySchema().addAdditionalProperties()
// additionalProperties is set to a string json schema and a string index signature is added to the generic interface
let schema2 = SchemaBuilder.emptySchema().addAdditionalProperties(SchemaBuilder.stringSchema())
/!\ Index signatures and type operations are not working well together. If you start using additionalProperties
in a schema, most of the transformation methods will fail after that. Try to use addAdditionalProperties
at the last step if possible
pickProperties & omitProperties
You can use this two methods to take a subset of the properties of the schema.
let schema = SchemaBuilder.emptySchema().addString("prop1").addBoolean("prop2")
// pickedSchema only contains "prop1"
let pickedSchema = schema.pickProperties(["prop1"])
// omitSchema only contains "prop1"
let omitSchema = schema.omitProperties(["prop2"])
pickAdditionalProperties
This method is a version of pickProperties
that supports additionalProperties
. You can keep the index signature, remove it or even restrict it to specific property names:
let schema = SchemaBuilder.emptySchema().addString("prop1").addBoolean("prop2").addAdditionalProperties(SchemaBuilder.stringSchema())
// pick properties and remove the index signature from the schema
let schemaWithoutIndexSignature = schema.pickAdditionalProperties(["prop1", "prop2"])
// pick properties and keep the index signature from the schema
let schemaWithIndexSignature = schema.pickAdditionalProperties(["prop1"], [])
// pick properties and keep only "prop3" from the index signature
let schemaWithOtherProperties = schema.pickAdditionalProperties(["prop1", "prop2"], ["prop3"])
mergeProperties
mergeProperties
method allows you to merge properties from the given schema into the current one. Properties that are defined in both schemas are merged using anyOf
operator (|
operator in Typescript).
let schema2 = SchemaBuilder.emptySchema().addArray("prop2", SchemaBuilder.stringSchema()).addNumber("prop3")
let schema = SchemaBuilder.emptySchema().addString("prop1").addBoolean("prop2").mergeProperties(schema2)
Which gives you following interface :
type T = {
prop1: string
prop3: number
prop2: boolean | string[]
}
overwriteProperties
overwriteProperties
method allows you to overwrite properties with the given schema. Properties that are defined in both schemas take the new type instead.
let schema2 = SchemaBuilder.emptySchema().addArray("prop2", SchemaBuilder.stringSchema()).addNumber("prop3")
let schema = SchemaBuilder.emptySchema().addString("prop1").addBoolean("prop2").overwriteProperties(schema2)
Which gives you following interface :
type T = {
prop1: string
prop2: string[]
prop3: number
}
transformProperties
transformProperties
method allows you to add a new type to existing properties. The json schema operator used is oneOf
and the typescript type operator is |
.
let schema = SchemaBuilder.emptySchema()
.addArray("prop1", SchemaBuilder.stringSchema())
.addBoolean("prop2")
.transformProperties(SchemaBuilder.stringSchema(), ["prop1"])
transformPropertiesToArray
transformPropertiesToArray
method allows you to transform existing properties to add an array version of it. The json schema operator used is oneOf
and the typescript type operator is |
.
Properties that are already arrays are not affected.
let schema = SchemaBuilder.emptySchema().addString("prop1").addBoolean("prop2").transformPropertiesToArray(["prop1"])
unwrapArrayProperties
unwrapArrayProperties
method allows you to transform existing array properties to add the generic type of the array to it. The json schema operator used is oneOf
and the typescript type operator is |
.
Properties that are not arrays are not affected.
let schema = SchemaBuilder.emptySchema().addArray("prop1", SchemaBuilder.stringSchema()).addBoolean("prop2").unwrapArrayProperties(["prop1"])
validate
validate
and validateList
methods allows you to easily run validation against your schema. Those two methods use Ajv
library. Validation functions are cached automatically. It uses the following default configuration :
new Ajv({
coerceTypes: false,
removeAdditional: false,
useDefaults: true,
strict: false,
})
You can override this configuration using the configureValidation
method.
You can also force the validation function to be cached right away schema.cacheValidationFunction()
and/or this.cacheListValidationFunction()
Literal Json Schema
SchemaBuilder
contains a fromJsonSchema
method that has the ability to deduce the type from the schema parameter directly. The schema has to be provided in a literal form using as const
.
For example:
let schemaBuilder = SchemaBuilder.fromJsonSchema({
type: "object",
properties: {
aString: {
type: "string",
description: "this is a test",
},
aBoolean: {
type: "boolean",
},
anInteger: {
type: "integer",
minimum: 0,
},
aSubObject: {
type: "object",
properties: {
aSubProperty: {
type: "number",
maximum: 100,
},
},
},
anArray: {
type: "array",
items: {
type: "string",
enum: ["a", "b", "c"],
},
},
},
required: ["aBoolean", "anArray"],
additionalProperties: false,
} as const)
Which gives you the following interface:
type T = {
aBoolean: boolean
anArray: ("a" | "b" | "c")[]
aString?: string
anInteger?: number
aSubObject?: {
aSubProperty: number
} & {
[k: string]: any
}
}
What's next?
schema-builder
is a component of Serafin framework. You can go to the main documentation to learn more about it : https://github.com/serafin-labs/serafin