darobin / web-schema

The meeting of JSON Schema and HTML form validation
14 stars 0 forks source link

Overview

NPM version

It is extremely common for web applications to be using JSON as the payload format to talk to a server backend. What's more, these are often edited through HTML forms. While JSON Schema is overall fine for general-purpose JSON validation, HTML forms do have some specificities that it would make sense to apply to validating JSON as well since those two technologies are brought to work together.

web-schema is a small library that intends to perform this union. It currently supports a subset of JSON Schema. I am in the process of adding new types that correspond to what HTML forms can do. Some are trivial (e.g. using "text" instead of "string" to signal that that schema item is best edited through a textarea) while others are more advanced (e.g. "html" for editable content, "email", "url", and other input types.).

Installing

The usual simple:

npm install web-schema

In order to run in a browser, you simply need to include the web-schema.js file, after having included underscore.js on which it depends.

Why not JSON Schema, one of the existing implementations?

The reason this library exists is because I initially needed JSON validation in a project. I tried to use some of the JSON Schema implementations that exist, but most were out of date. Those that weren't tended to break in the contexts in which I was using them, and were generally rather larger and more complex than my needs. It turned out to be simpler to just write a validator that matches my needs.

What's more, I needed to generate HTML forms from the schema. This is of course possible using JSON Schema, but in order to be properly functional it requires hinting. That seemed to be a mismatch that could be addressed by rendering the schema language closer to what HTML forms can capture.

I release this library on the assumption that if I needed it, others might too. I may add support for more of JSON Schema, and I will certainly take pull requests (so long as they don't cause excessive bloat). I am in the process of adding more types to map to forms.

One notable difference with JSON Schema is that it supports schemata referencing one another. Doing that is an explicit non-goal of this library. But it ought to be easy to implement JSON referencing separately (in another small library) and feed schemata with references resolved into web-schema.

If you are familiar with JSON Schema you won't be lost here: a lot is shared.

API

The API is very simple, the following example probably tells you all you need to know:

var ws = new WebSchema();
var result = ws.validate(object, schema);
if (result.ok) {
    // victory \o/
}
else {
    console.log("JSON has the following errors: " + result.errors.join(", ") + " at path " + result.path);
}

var ws = new WebSchema()

A simple constructor that takes no arguments.

var result = ws.validate(object, schema);

This takes an object that is parsed JSON (or any in-memory equivalent) and a schema that corresponds to the schema language described in the following section. Note that validate() will throw if you schema is invalid.

The return value is an object with the following fields:

Supported schema constructs

A schema is basically a nested structure of objects that describe the constraints on a JSON instance at that nesting level. Each of these objects is keyed off a specific fundamental type, and the constraints that can be applied to the value depend on that fundamental type.

null

The object is null.

Example schema:

{ "type": "null" }

No additional constraints.

any

Absolutely anything goes here.

Example schema:

{ "type": "any" }

No additional constraints.

boolean

The object is a boolean value (true or false). Note that this is strictly for booleans, and not for values that are truthy or falsy.

Example schema:

{ "type": "boolean" }

Other example:

{
    "type": "boolean"
,   "enum": [true]
}

Additional constraints:

string or text

The object is a string. A number of constraints on length, patterns, and enumerations apply. Note that these are strictly strings and not objects that stringify.

The "text" type is also possible and is strictly equivalent to "string". The primary difference is that you would expect to map it to textarea rather than input.

Example schemata:

{ "type": "string" }

{
    "type": "string"
,   "enum": ["carrot", "banana", "donkey"]
}

{
    "type": "string"
,   "pattern": "a{3}\\d\\d"
}

{
    "type": "string"
,   "minLength": 2
,   "maxLength": 17
}

Additional constraints:

number

The object is a number. It can be further constrained on enumeration, minimum, and maximum values.

Example schemata:

{ "type": "number" }

{
    "type": "number"
,   "enum": [5, 17, 23, 42]
}

{
    "type": "number"
,   "minimum": 23
,   "exclusiveMaximum": 57.2
}

Additional constraints:

object

The object is, well, an object. Its properties can be enumerated and themselves recursively defined and constrained.

Example schemata:

{ "type": "object" }

{
    "type": "object"
,   "properties": {
        "fullName":  { "type": "string", "required": true }
    ,   "age":       { "type": "number", "minimum": 0 }
    ,   "superHeroIdentity": {
            "type": "object"
        ,   "coolName": { "type": "string" }
        ,   "superPower": {
                "type": "string"
            ,   "enum": ["flying", "telekinesis", "parsing MIME"]
            }
        }
    }
}

Additional constraint:

In addition to its regular type information, each property value can also take a boolean constraint called required. If set to true, then this field must be present in the instance (the default is for it to be optional).

array

The object is an array. This can be further constrained with minimal and maximal lengths, as well as with constraints on the types of the objects contained in the array.

Example schemata:

{ "type": "array" }

{
    "type":         "array"
,   "minItems":     3
,   "maxItems":     200
,   "items":        { "type": "number" }
,   "uniqueItems":  true
}

{
    "type":             "array"
,   "items":            [ { "type": "number" }, { "type": "string" }, { "type": "string" } ]
,   "additionalItems":  true
}

Additional constraints:

Union types

It is also possible to define union types, which is to say, cases in which validation will be tried against multiple type definitions until one matches or the list of options in the union is exhausted.

Example schemata:

{ "type": ["number", "string"] }

{
    "type": [
        { "type": "string", "pattern": "^\\w+$" }
    ,   { "type": "array", "minItems": 3 }
    ,   "boolean"
    ]
}

A union is defined by providing an array of options for the type. Each item in that array may be either just the string name of a fundamental type, or a full-fledged type definition. If none of the types match, then the validation fails.

Date

This type describes a date. It is captured in JSON as a string that needs to match: YYYY-MM-DD.

Example schema:

{
    "type":         "date"
,   "description":  "Date"
}

It currently takes no additional constraints, but it likely will in the future.

Time

This type describes a time without associated time zone information. It is captured in JSON as a string that needs to match: HH:MM:SS.SSS where the seconds are optional and the second fragments can have between one and three digits.

Example schema:

{
    "type":         "time"
,   "description":  "Time"
}

It currently takes no additional constraints, but it likely will in the future.

Datetime Local

This type describes a date and time without associated time zone information. It is captured in JSON as a string that needs to match: YYYY-MM-DDTHH:MM:SS.SSS where the seconds are optional and the second fragments can have between one and three digits.

Example schema:

{
    "type":         "datetime-local"
,   "description":  "Date and time"
}

It currently takes no additional constraints, but it likely will in the future.