docschema
v1.2.8
Published
Schema declaration and validation library using JsDoc comments
Downloads
212
Maintainers
Readme
DocSchema
DocSchema allows you to create schemas from JsDoc comments and use them to validate data at runtime.
Table of Contents
- Key Features
- Installation
- Basic Usage
- Preserve JsDoc Comments
- Schemas
- External Types
- Filters
- Error Handling
- How it Works?
- How to Use?
Key Features
- 100% JavaScript, uses JsDoc comments to make schemas
- ES6 imports
- Works in Node.js and modern browsers *
- Zero dependencies
- Validates the types and has additional filters to validate the value
- Comes with typings
Installation
npm install docschema
pnpm add docschema
yarn add docschema
Basic Usage
- Use
docSchema()
to create a schema:
import { docSchema } from 'docschema'
// import docSchema from 'docschema' // Also valid
/**
* @param {string} name
* @param {number} age
*/
const personSchema = docSchema()
- Validate your data:
const correctData = { name: 'John', age: 31 }
const wrongData = { name: 'John', age: '31' }
personSchema.validate(correctData) // Returns the input value
personSchema.validate(wrongData) // Throws ValidationError
- Or, check your data:
const correctData = { name: 'John', age: 31 }
const wrongData = { name: 'John', age: '31' }
personSchema.check(correctData) // Returns an object (explained later)
personSchema.check(wrongData) // Returns an object with the error data
- Or, approve your data:
const correctData = { name: 'John', age: 31 }
const wrongData = { name: 'John', age: '31' }
personSchema.approves(correctData) // Returns true
personSchema.approves(wrongData) // Returns false
Preserve JsDoc Comments
When JavaScript files are minified, JsDoc comments are removed. We don't want that to
happen to our JsDoc schemas. To preserve them, try using @preserve
or @license
tag:
/**
* @preserve
* @param {string} name
* @param {number} age
*/
Schemas
To define a schema, some of the standard JsDoc tags are used, but not in the way they
are supposed to be used. The tags are @enum
, @typedef
and @param
. They are chosen,
because when they are used directly above a constant or variable definition, they don't
set the type of that constant or variable.
@enum
For a simple type that is only one word:
/**
* @enum {string}
*/
const schema = docSchema()
schema.validate('John')
For an object:
/**
* @enum {{ name: string, age: number }}
*/
const personSchema = docSchema()
personSchema.validate({ name: 'John', age: 31 })
Why @enum
, but not @type
? Because @type
sets the type of the constant below,
which is something we don't want in DocSchema. What we want is to use the information in
the JsDoc comment as a schema, for the purpose of validate()
.
@typedef
For an object, you can also use one @typedef
tag with type Object
and one or more
@property
tags:
/**
* @typedef {Object} PersonSchema
* @property {string} name
* @property {number} age
*/
const personSchema = docSchema()
personSchema.validate({ name: 'John', age: 31 })
Now not only you can do the validations, but the type PersonSchema
can alo be used
somewhere else in the same file. The use case for this will be demonstrated later.
By the way, this variation is also valid:
/**
* @typedef PersonSchema
* @type {Object}
* @property {string} name
* @property {number} age
*/
const personSchema = docSchema()
personSchema.validate({ name: 'John', age: 31 })
@param
For an object, you can also use one or more @param
tags:
/**
* @param {string} name
* @param {number} age
*/
const personSchema = docSchema()
personSchema.validate({ name: 'John', age: 31 })
However, the type information can only be used to make the schema for personSchema
.
This can be used in a very narrow scope.
External Types
In schema comments, you can use constructor types. For example:
class MyClass { ... }
/**
* @param {Date} createdAt - createdAt must be an instance of Date
* @property {MyClass} myClass - myClass must be an instance of MyClass
*/
Also, you can define custom typedefs:
/**
* @typedef PersonSchema
* @type {Object}
* @property {string} name
* @property {number} age
*/
/**
* @enum {PersonSchema}
*/
const personSchema = docSchema()
personSchema.validate({ name: 'John', age: 31 })
It's also possible to use ambient typedefs from external files.
Ambient typedefs are typedefs, located in a file without imports or exports.
Usually you don't have to import these files anywhere, as the types defined in them
are global (ambient) anyway. However, for DocSchema you must import them.
DocSchema does not scan for external files with ambient types by itself,
but it will read external files, imported like this: import './fileName.js'
:
//---- ambientTypedefs.js ----//
// This file should not have imports or exports
/**
* @typedef PersonSchema
* @type {Object}
* @property {string} name
* @property {number} age
*/
//---- index.js ----//
import './ambientTypedefs.js'
/**
* @enum {PersonSchema}
*/
const personSchema = docSchema()
personSchema.validate({ name: 'John', age: 31 })
Strict
/**
* @typedef PersonSchema
* @type {Object}
* @property {string} name
* @property {number} age
* @strict
*/
const personSchema = docSchema()
personSchema.validate({ name: 'John', age: 31, child: 'Jane' })
With @strict
, the validation will not be successful if the input object
contains properties that are not in the schema. In the example above, the
object contains one extra property child
that doesn't exist in the schema.
Filters
Sometimes validating only the type is not enough. Maybe you want a number with minimum value, or a string with certain contents? Use filters for that.
The syntax is like a normal JavaScript Object, but only some key-value pairs would work.
The filters are placed in the description section of each parameter. Unfortunately,
because of that in your IDE you will not get linting or code completion for the filters.
However, if you define them incorrectly, you should get errors
(SyntaxError
)
at parse time.
In the example below, there are two filters: { min: 1 }
and { min: 18 }
:
/**
* @param {string} name - { min: 1 }
* @param {number} age - { min: 18 }
*/
const personSchema = docSchema()
personSchema.validate({ name: 'John', age: 31 }) // Pass
personSchema.validate({ name: '', age: 31 }) // Throws ValidationError
personSchema.validate({ name: 'John', age: 15 }) // Throws ValidationError
And you can also add an actual description for each parameter, which can be placed before, after or even on both sides of the filter object:
/**
* @param {string} name - The name should not be empty { min: 1 }
* @param {number} age - { min: 18 } Adults only
*/
const personSchema = docSchema()
The dash in the description is optional:
/**
* @param {string} name The name should not be empty { min: 1 }
* @param {number} age { min: 18 } Adults only
*/
const personSchema = docSchema()
Also, the descriptions can be in multiple rows. In this case, keep in mind that the description of each parameter starts after its name and ends where the next tag is defined.
Note that these descriptions are NOT custom error messages. They are just regular JsDoc descriptions.
/**
* @param {string} name The name should not be empty
* { min: 1 }
* @param {number} age Adults only
* { min: 18 }
*/
const personSchema = docSchema()
Filters also work with object literal syntax. In this case, the description section looks more like a comment.
Note: Having descriptions like that is not a standard practice in JsDoc and you may get an error in your IDE, or depending on your ESLint settings.
/**
* @enum {{
* name: string, // { min: 1 }
* age: number, // { min: 18 }
* }}
*/
const personSchema = docSchema()
Array-specific filters
- With
numeric
value:min
- Minimum length of the array.max
- Maximum length of the array.length
- Exact length of the array.
Number-specific filters
- With
numeric
value:min
- Minimum value (gte
alias).max
- Maximum value (lte
alias).gte
- Greater than or equal.gte
- Greater than or equal.gt
- Greater than.lt
- Lower than.step
- The number must be divisible by the number of "step".
- With boolean value (either true or false):
int
- The number must (or not) be integer.finite
- The number must (or not) be finite (notInfinity
and not-Infinity
)safeInt
- The number must (or not) be safe integer (betweenNumber.MIN_SAFE_INTEGER
andNumber.MAX_SAFE_INTEGER
)
String-specific filters
- With
numeric
value:min
- Minimum characters in the string.max
- Maximum characters in the string.length
- Exact amount of characters in the string.
- With
string
value:startsWith
- The string must start with the specified string.endsWith
- The string must end with the specified string.includes
- The string must include the specified string.excludes
- The string must not include the specified string.url
- The string must be a valid URL.
- With
boolean
value (either true or false):email
- The string must (or not) be a valid email name.ip
- The string must (or not) be a valid IPv4 or IPv6 address.ipv4
- The string must (or not) be a valid IPv4 address.ipv6
- The string must (or not) be a valid IPv6 address.cuid
- The string must (or not) be a valid CUID.cuid2
- The string must (or not) be a valid CUID2.ulid
- The string must (or not) be a valid ULID.uuid
- The string must (or not) be a valid UUID.
- With
RegExp
value:pattern
- Regex pattern.RegExp
value.
Custom error messages
On each filter, you can specify a custom error message. To do that, replace the filter value with a tuple (array with 2 values) where the filter value is at index 0 and the custom error message is at index 1. For example:
/**
* @param {string} name { min: [ 1, 'The name should not be empty' ] }
* @param {number} age { min: [ 18, 'Adults only' ] }
*/
const personSchema = docSchema()
Or:
/**
* @enum {{
* name: string, // { min: [ 1, 'The name should not be empty' ] }
* age: number, // { min: [ 18, 'Adults only' ] }
* }}
*/
const personSchema = docSchema()
Error Handling
ValidationError
is a child class of Error
and is thrown only when using
.validate()
. It contains some additional properties,
providing information about the validation error. The same properties are returned by
.check()
, but as an object.
On validation success, the properties are mostly empty, except pass
and tag
:
{
pass: true,
tag: 'enum',
// The others are empty
message: '',
kind: '',
expectedType: '',
filter: undefined,
value: undefined,
valuePath: []
}
On validation failure, the properties are as follows:
pass
-false
.tag
- String. The name of the tag where the validation failed. For example"enum"
,message
- String. Error message, containing information about the problem.kind
-"type"
on type checking failure,"filter"
on filter checking failure, or "strict" on failure when@strict
is used.expectedType
- String. The expected type."param"
,"property"
.filter
- Object with two properties - name and value. It appears on filter validation failure only and provides information about the filter that was used. For example, on input number 5:{ name: "min", value: 10 }
value
- The value at which the validation failure happened.valuePath
- Array. The path to the value at which the validation failure happened, as a separate string values in the array. For example:- With simple value it is an empty array
[]
. - With Object
{ foo: { bar: "wrong-value" } }
it is like this:[ "foo", "bar" ]
. - With Array
{ foo: [ "wrong-value" ] }
it is like this:[ "foo", "0" ]
- With simple value it is an empty array
How it Works?
- When
docSchema()
is called, a newError
is thrown internally and its call stack is captured. - From the captured stack, information about the location of
docSchema()
is extracted - file name, line and column. - The file (where
docSchema()
is located) is read synchronously. In browsers, synchronous XHR request is used. Yes, this is deprecated for a good reason, but in our case we already got the file in the browser's cache. - All JsDoc comments in the file are extracted and parsed. We know where to locate our
comment, it must be at the lines just above
docSchema()
. From our JsDoc comment we got a schema, and that schema is returned bydocSchema()
.
How to Use?
TypeScript?
DocSchema is intended to be used in JavaScript & JsDoc environment, along with
TypeScript for type-checking only. TypeScript understands JsDoc comments very well,
which allows us to use them for type definitions. For this, TypeScript needs to be
configured with allowJs
and checkJs set to true
.
DocSchema itself is configured like this, and you can see its tsconfig.js
file for
reference.
Infer Type from Schema
Ideally, we want to use the same JsDoc type definition for the schema and as a type.
With @param
we have a problem:
/**
* @param {string} name
* @param {number} age
*/
const personSchema = docSchema()
Here the JsDoc comment is consumed by docSchema()
, but it can't be used as a type
somewhere else. personSchema
has the type that is returned by docSchema()
.
With @typedef
:
/**
* @typedef {Object} PersonSchema
* @property {string} name
* @property {number} age
*/
const personSchema = docSchema()
Here we have a type PersonSchema
and it can be used somewhere else in the same file.
We can't use PersonSchema
outside this file. And it's probably not a good idea to use
PersonSchema
as a type in the same file anyway.
With @enum
:
/**
* @enum {{ name: string, age: number }}
*/
const PersonSchema = docSchema()
/**
* @type {PersonSchema}
*/
const person = { name: 'John', age: 31 }
// Validate
PersonSchema.validate(person)
Something interesting happens here. PersonSchema
now acts both as a schema validator
and as a type.
TypeScript will give you an error if you assign a value to person
that doesn't match
the type in the @enum
:
/**
* @type {PersonSchema}
*/
const person = 'John'
// TS2322: Type 'string' is not assignable to type 'PersonSchema'
/**
* @type {PersonSchema}
*/
const person = { name: 'John', age: '31' }
// TS2322: Type 'string' is not assignable to type 'number'
TypeScript will also give you an error if you are not using PersonSchema
properly:
// Validate
PersonSchema.validateee(person)
// TS2551: Property 'validateee' does not exist on type 'DocSchema'. Did you mean 'validate'?
This means that we can also separate our schemas in a separate file, like this:
//---- schemas.js ----//
import { docSchema } from 'docschema'
/**
* @enum {{ name: string, age: number }}
*/
const PersonSchema = docSchema()
export { PersonSchema }
//---- index.js ----//
import { PersonSchema } from './schemas.js'
/**
* @type {PersonSchema}
*/
const person = { name: 'John', age: 31 }
PersonSchema.validate(person)