npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@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