npm package: @iopa/schema-router
Description: Fast Json Schema Validator and Router for IOPA Applications
Author: iopa
Latest version: 4.0.23

# [![IOPA](https://iopa.io/iopa.png)](https://iopa.io)<br> @iopa/schema-router

[![NPM](https://img.shields.io/badge/iopa-certified-99cc33.svg?style=flat-square)](https://iopa.io/)

[![NPM](https://nodei.co/npm/@iopa/schema-router.png?downloads=true)](https://nodei.co/npm/@iopa/schema-router/)

## About

`@iopa/schema-router` is a compiled schema validator and router for the IOPA framework

It validates JSON Schema and JSON Type Definitions for both inbound requests and outbound responses

## Usage

### Routing

Include whereever you would otherwise use `@iopa/router`

See `@iopa/router` for all routing documentation


### Installation

```ts
import { RouterApp } from 'iopa'
import SchemaRouter, { type ISchemaAppOptions } from '@iopa/schema-router'

const app: ISchemaApp = new RouterApp()

//
// . . . .
//

app.use<ISchemaAppOptions>(SchemaRouter, 'Schema Router', {
  jsonShorthand: false
})

```

``` ts
app.get(
  '/api/helloworld-us-only',
  async (context) => {
    return 'Hello World'
  },
  {
    schema: {
      headers: {
        type: 'object',
        properties: { 'cf-ipcountry': { type: 'string', pattern: '^US$' } },
        required: ['cf-ipcountry']
      }
    }
  }
)
```

## Validation and Serialization
Iopa Schema Router uses a schema-based approach, and even if it is not mandatory we
recommend using [JSON Schema](https://json-schema.org/) to validate your routes
and serialize your outputs. Internally, Iopa Schema Router compiles the schema into a
highly performant function.

Validation will only be attempted if the content type is `application-json`, as
described in the documentation for the [content type
parser](./ContentTypeParser.md).

All the examples in this section are using the [JSON Schema Draft
7](https://json-schema.org/specification-links.html#draft-7) specification.

> ## ⚠  Security Notice
> Treat the schema definition as application code. Validation and serialization
> features dynamically evaluate code with `new Function()`, which is not safe to
> use with user-provided schemas. See [Ajv](https://npm.im/ajv) and
> [fast-json-stringify](https://npm.im/fast-json-stringify) for more details.
>
> Moreover, the [`$async` Ajv
> feature](https://ajv.js.org/guide/async-validation.html) should not be used as
> part of the first validation strategy. This option is used to access Databases
> and reading them during the validation process may lead to Denial of Service
> Attacks to your application. 


### Core concepts
The validation and the serialization tasks are processed by two different, and
customizable, actors:
- [Ajv v8](https://www.npmjs.com/package/ajv) for the validation of a request
- [fast-json-stringify](https://www.npmjs.com/package/fast-json-stringify) for
  the serialization of a response's body

These two separate entities share only the JSON schemas added to Iopa
app instance through `.addSchema(schema)`.

#### Adding a shared schema
<a id="shared-schema"></a>

Thanks to the `addSchema` API added to the Iopa app instance, you can add multiple schemas 
to the Iopa Schema Router and then reuse them in multiple parts of your application.

The shared schemas can be reused through the JSON Schema
[**`$ref`**](https://tools.ietf.org/html/draft-handrews-json-schema-01#section-8)
keyword. Here is an overview of _how_ references work:

+ `myField: { $ref: '#foo'}` will search for field with `$id: '#foo'` inside the
  current schema
+ `myField: { $ref: '#/definitions/foo'}` will search for field
  `definitions.foo` inside the current schema
+ `myField: { $ref: 'http://url.com/sh.json#'}` will search for a shared schema
  added with `$id: 'http://url.com/sh.json'`
+ `myField: { $ref: 'http://url.com/sh.json#/definitions/foo'}` will search for
  a shared schema added with `$id: 'http://url.com/sh.json'` and will use the
  field `definitions.foo`
+ `myField: { $ref: 'http://url.com/sh.json#foo'}` will search for a shared
  schema added with `$id: 'http://url.com/sh.json'` and it will look inside of
  it for object with `$id: '#foo'`


**Simple usage:**

```js
app.addSchema({
  $id: 'http://example.com/',
  type: 'object',
  properties: {
    hello: { type: 'string' }
  }
})

app.post('/', 
  myHandlerFunction,
  {
  schema: {
    body: {
      type: 'array',
      items: { $ref: 'http://example.com#/properties/hello' }
    }
  }
})
```

**`$ref` as root reference:**

```js
app.addSchema({
  $id: 'commonSchema',
  type: 'object',
  properties: {
    hello: { type: 'string' }
  }
})

a[[]].post('/', {
  myHandlerFunction,
  schema: {
    body: { $ref: 'commonSchema#' },
    headers: { $ref: 'commonSchema#' }
  }
})
```

#### Retrieving the shared schemas
<a id="get-shared-schema"></a>

If the validator and the serializer are customized, the `.addSchema` method will
not be useful since the actors are no longer controlled by Iopa Schema Router. To access
the schemas added to the Iopa Schema Router instance, you can simply use `.getSchemas()`
available on the `urn:io.iopa.schema:controller` capability:

```js
app.capability('urn:io.iopa.schema:controller').addSchema({
  $id: 'schemaId',
  type: 'object',
  properties: {
    hello: { type: 'string' }
  }
})

const mySchemas = app.capability('urn:io.iopa.schema:controller').getSchemas()
const mySchema = app.capability('urn:io.iopa.schema:controller').getSchema('schemaId')
```

The function `getSchemas` returns the shared
schemas available in the selected scope:

```js
app.addSchema({ $id: 'one', my: 'hello' })
// will return only `one` schema
app.get('/', (request, reply) => { reply.send(app.capability('urn:io.iopa.schema:controller').getSchemas()) })
```


### Validation
The route validation internally relies upon [Ajv
v8](https://www.npmjs.com/package/ajv) which is a high-performance JSON Schema
validator. Validating the input is very easy: just add the fields that you need
inside the route schema, and you are done!

The supported validations are:
- `body`: validates the body of the request if it is a POST, PUT, or PATCH
  method.
- `querystring` or `query`: validates the query string.
- `params`: validates the route params.
- `headers`: validates the request headers.

All the validations can be a complete JSON Schema object (with a `type` property
of `'object'` and a `'properties'` object containing parameters) or a simpler
variation in which the `type` and `properties` attributes are forgone and the
parameters are listed at the top level (see the example below).

> ℹ If you need to use the latest version of Ajv (v8) you should read how to do
> it in the [`schemaController`](./Server.md#schema-controller) section.

Example:
```js
const bodyJsonSchema = {
  type: 'object',
  required: ['requiredKey'],
  properties: {
    someKey: { type: 'string' },
    someOtherKey: { type: 'number' },
    requiredKey: {
      type: 'array',
      maxItems: 3,
      items: { type: 'integer' }
    },
    nullableKey: { type: ['number', 'null'] }, // or { type: 'number', nullable: true }
    multipleTypesKey: { type: ['boolean', 'number'] },
    multipleRestrictedTypesKey: {
      oneOf: [
        { type: 'string', maxLength: 5 },
        { type: 'number', minimum: 10 }
      ]
    },
    enumKey: {
      type: 'string',
      enum: ['John', 'Foo']
    },
    notTypeKey: {
      not: { type: 'array' }
    }
  }
}

const queryStringJsonSchema = {
  type: 'object',
  properties: {
    name: { type: 'string' },
    excitement: { type: 'integer' }
  }
}

const paramsJsonSchema = {
  type: 'object',
  properties: {
    par1: { type: 'string' },
    par2: { type: 'number' }
  }
}

const headersJsonSchema = {
  type: 'object',
  properties: {
    'x-foo': { type: 'string' }
  },
  required: ['x-foo']
}

const schema = {
  body: bodyJsonSchema,
  querystring: queryStringJsonSchema,
  params: paramsJsonSchema,
  headers: headersJsonSchema
}

app.post('/the/url', { schema }, handler)
```

*Note that Ajv will try to [coerce](https://ajv.js.org/coercion.html) the values
to the types specified in your schema `type` keywords, both to pass the
validation and to use the correctly typed data afterwards.*

The Ajv default configuration in Iopa Schema Router supports coercing array parameters in
`querystring`. Example:

```js
const opts = {
  schema: {
    querystring: {
      type: 'object',
      properties: {
        ids: {
          type: 'array',
          default: []
        },
      },
    }
  }
}

app.get('/', opts, (request, reply) => {
  reply.send({ params: request.query }) // echo the querystring
})

server.listen({ port: 3000 }, (err) => {
  if (err) throw err
})
```

#### Ajv Plugins
<a id="ajv-plugins"></a>

You can provide a list of plugins you want to use with the default `ajv`
instance. Note that the plugin must be **compatible with the Ajv version shipped
within Iopa Schema Router**.

> Refer to [`ajv options`](./Server.md#ajv) to check plugins format

```js
app.use<ISchemaAppOptions>(SchemaRouter, 'Schema Router', {
  ajv: {
    plugins: [
      require('ajv-merge-patch')
    ]
  }
})

app.post('/', (context, next) => { return { ok: 1 }},
{
  schema: {
    body: {
      $patch: {
        source: {
          type: 'object',
          properties: {
            q: {
              type: 'string'
            }
          }
        },
        with: [
          {
            op: 'add',
            path: '/properties/q',
            value: { type: 'number' }
          }
        ]
      }
    }
  }
})

app.post('/foo', (context, next) => { return { ok: 1 }},
{
  schema: {
    body: {
      $merge: {
        source: {
          type: 'object',
          properties: {
            q: {
              type: 'string'
            }
          }
        },
        with: {
          required: ['q']
        }
      }
    }
  }
})
```

#### Schema Validator Configuration
<a id="schema-validator"></a>

Iopa Schema Router's baseline ajv configuration is:

```js
{
  coerceTypes: true, // change data type of data to match type keyword
  useDefaults: true, // replace missing properties and items with the values from corresponding default keyword
  removeAdditional: true, // remove additional properties
  // Explicitly set allErrors to `false`.
  // When set to `true`, a DoS attack is possible.
  allErrors: false
}
```

This baseline configuration can be modified by providing `customOptions` to your Iopa Schema Router use statement.

### Serialization
<a id="serialization"></a>

Usually, you will send your data to the clients as JSON, and Iopa Schema Router has a
powerful tool to help you,
[fast-json-stringify](https://www.npmjs.com/package/fast-json-stringify), which
is used if you have provided an output schema in the route options. We encourage
you to use an output schema, as it can drastically increase throughput and help
prevent accidental disclosure of sensitive information.

Example:
```js
const schema = {
  response: {
    200: {
      type: 'object',
      properties: {
        value: { type: 'string' },
        otherValue: { type: 'boolean' }
      }
    }
  }
}

app.post('/the/url', handler, { schema })
```

As you can see, the response schema is based on the status code. If you want to
use the same schema for multiple status codes, you can use `'2xx'` or `default`,
for example:
```js
const schema = {
  response: {
    default: {
      type: 'object',
      properties: {
        error: {
          type: 'boolean',
          default: true
        }
      }
    },
    '2xx': {
      type: 'object',
      properties: {
        value: { type: 'string' },
        otherValue: { type: 'boolean' }
      }
    },
    201: {
      // the contract syntax
      value: { type: 'string' }
    }
  }
}

app.post('/the/url', handler, { schema })
```

### Error Handling
When schema validation fails for a request, Iopa Schema Router will automatically return a
status 400 response including the result from the validator in the payload. As
an example, if you have the following schema for your route

```js
const schema = {
  body: {
    type: 'object',
    properties: {
      name: { type: 'string' }
    },
    required: ['name']
  }
}
```

and fail to satisfy it, the route will immediately return a response with the
following payload

```js
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "body should have required property 'name'"
}
```

### JSON Schema support

JSON Schema provides utilities to optimize your schemas that, in conjunction
with Iopa Schema Router's shared schema, let you reuse all your schemas easily.

| Use Case                          | Validator | Serializer |
|-----------------------------------|-----------|------------|
| `$ref` to `$id`                   | ️️✔️ | ✔️ |
| `$ref` to `/definitions`          | ✔️ | ✔️ |
| `$ref` to shared schema `$id`          | ✔️ | ✔️ |
| `$ref` to shared schema `/definitions` | ✔️ | ✔️ |

#### Examples

##### Usage of `$ref` to `$id` in same JSON Schema

```js
const refToId = {
  type: 'object',
  definitions: {
    foo: {
      $id: '#address',
      type: 'object',
      properties: {
        city: { type: 'string' }
      }
    }
  },
  properties: {
    home: { $ref: '#address' },
    work: { $ref: '#address' }
  }
}
```


##### Usage of `$ref` to `/definitions` in same JSON Schema
```js
const refToDefinitions = {
  type: 'object',
  definitions: {
    foo: {
      $id: '#address',
      type: 'object',
      properties: {
        city: { type: 'string' }
      }
    }
  },
  properties: {
    home: { $ref: '#/definitions/foo' },
    work: { $ref: '#/definitions/foo' }
  }
}
```

##### Usage `$ref` to a shared schema `$id` as external schema
```js
app.addSchema({
  $id: 'http://foo/common.json',
  type: 'object',
  definitions: {
    foo: {
      $id: '#address',
      type: 'object',
      properties: {
        city: { type: 'string' }
      }
    }
  }
})

const refToSharedSchemaId = {
  type: 'object',
  properties: {
    home: { $ref: 'http://foo/common.json#address' },
    work: { $ref: 'http://foo/common.json#address' }
  }
}
```

##### Usage `$ref` to a shared schema `/definitions` as external schema
```js
app.addSchema({
  $id: 'http://foo/shared.json',
  type: 'object',
  definitions: {
    foo: {
      type: 'object',
      properties: {
        city: { type: 'string' }
      }
    }
  }
})

const refToSharedSchemaDefinitions = {
  type: 'object',
  properties: {
    home: { $ref: 'http://foo/shared.json#/definitions/foo' },
    work: { $ref: 'http://foo/shared.json#/definitions/foo' }
  }
}
```

### Resources
<a id="resources"></a>

- [JSON Schema](https://json-schema.org/)
- [Understanding JSON
  Schema](https://spacetelescope.github.io/understanding-json-schema/)
- [fast-json-stringify
  documentation](https://github.com/fastify/fast-json-stringify)
- [Ajv documentation](https://github.com/epoberezkin/ajv/blob/master/README.md)
- [Ajv i18n](https://github.com/epoberezkin/ajv-i18n)
- [Ajv custom errors](https://github.com/epoberezkin/ajv-errors)


## License

MIT