Security News
Cloudflare Adds Security.txt Setup Wizard
Cloudflare has launched a setup wizard allowing users to easily create and manage a security.txt file for vulnerability disclosure on their websites.
schema-to-yup
Advanced tools
Build a Yup schema object to validate models from a domain model schema (JSON or GraphQL)
Build a Yup schema from a JSON Schema, GraphQL schema (type definition) or any other similar type/class and field/properties model or schema :)
The builder currently supports the most commonly used JSON Schema layout and GraphQL type definition exports using graphSchemaToJson (see GraphQL schema).
It also supports some extra convenience schema properties that make it more "smooth" to define validation requirements declaratively (see below).
According to the JSON schema specs, you are free to add extra metadata to the field schema definitions beyond those supported "natively".
Install
npm install schema-to-yup -S
or yarn add schema-to-yup
Use
const schema = {
$schema: "http://json-schema.org/draft-07/schema#",
$id: "http://example.com/person.schema.json",
title: "Person",
description: "A person",
type: "object",
properties: {
name: {
description: "Name of the person",
type: "string"
},
email: {
type: "string",
format: "email"
},
fooorbar: {
type: "string",
matches: "(foo|bar)"
},
age: {
description: "Age of person",
type: "number",
exclusiveMinimum: 0,
required: true
},
characterType: {
enum: ["good", "bad"],
enum_titles: ["Good", "Bad"],
type: "string",
title: "Type of people",
propertyOrder: 3
}
},
required: ["name", "email"]
};
const config = {
// for error messages...
errMessages: {
age: {
required: "A person must have an age"
},
email: {
required: "You must enter an email address",
format: "Not a valid email address"
}
}
};
const { buildYup } = require("schema-to-yup");
const yupSchema = buildYup(json, config);
// console.dir(schema)
const valid = await yupSchema.isValid({
name: "jimmy",
age: 24
});
console.log({
valid
});
// => {valid: true}
This would generate the following Yup validation schema:
const schema = yup.object().shape({
name: yup.string().required(),
age: yup
.number()
.required()
.positive()
});
Note the "required": true
for the age
property (not natively supported by JSON schema).
Please note that this library does not currently resolve $ref
(JSON Pointers) out of the box. You can use another library for that.
You could f.ex use json-schema-ref-parser to preprocess your schema. Also see:
By default, any property will be explicitly notRequired
unless set to be required, either via required: true
in the property constraint object or via the required
list of properties of the object
schema definition (of the property).
You can override the notRequired
behavior by setting it on the new mode
object of the configuration which can be used to control and fine-tune runtime behaviour.
const jsonSchema = {
title: "users",
type: "object",
properties: {
username: { type: "string" }
}
};
const yupSchema = buildYup(jsonSchema, {
mode: {
notRequired: true // default setting
}
});
// will be valid since username is not required by default
const valid = yupSchema.validateSync({
foo: "dfds"
});
const yupSchema = buildYup(jsonSchema, {
mode: {
notRequired: true // default setting
}
});
// will be invalid since username is required by default when notRequired mode is disabled
const valid = yupSchema.validateSync({
foo: "dfds"
});
The new run time mode settings are demonstrated under sample-runs/mode
No validation error (prop not required unless explicitly specified):
$ npx babel-node sample-runs/modes/not-required-on.js
Validation error if not valid type:
$ npx babel-node sample-runs/modes/not-required-on.js
You can access the internal Yup shape, via shapeConfig
on the yup schema returned by the buildYup
schema builder function.
This allows you to easily mix and match to suit more advanced requirements.
const { buildYup } = require("json-schema-to-yup");
const { shapeConfig } = buildYup(json, config);
const schema = yup.object().shape({
...shapeConfig,
...customShapeConfig
});
Currently the following schema types are supported:
array
boolean
date
number
object
string
You can pass any custom typehandlers in a typeHandlers
object as part of the config
object passes. See setTypeHandlers()
in entry.js
for how this works internally.
function myCustomStringHandler = (obj, config) => {
// ... custom handler
// return yup type schema such as yup.string()
// with one or more constraints added
}
const yupSchema = buildYup(jsonSchema, {
typeHandlers: {
string: myCustomStringHandler
},
});
This can be used to support special cases, to circumvent a bug or unexpected/unwarranted behaviour for one or more of the built-in type handlers etc. You can then use the built in classes as building blocks.
Release 2.0
will likely include a much improved infrastructure of suitable building blocks to easily customize behavior and extend the functionality etc.
Version 1.9.18
and higher supports using a custom constraint builder to add and build constraints. All factories are initialised in initHelpers
and executed as the first step of convert
(see mixed.js
)
class MyConstraintBuilder extends ConstraintBuilder {
constructor(typeHandler, opts = {}) {
super(typeHandler, opts);
// custom instance configuration
}
build(propName, opts) {
/// custom build logic
// returns new type validation handler (base) with built constraint added
return newBase;
}
addConstraint(propName, opts) {
// custom add constraint logic
return this.typeHandler;
}
// custom event handler
onConstraintAdded({ name, value }) {
// ...
return this.typeHandler;
}
}
To use a custom constraint builder we recommend subclassing the ConstraintBuilder
class that comes with the library. Then create a factory method thart can instanciate it.
const createConstraintBuilder = (typeHandler, config) => {
return new MyConstraintBuilder(typeHandler, config);
};
const config = {
createConstraintBuilder
// ... more configuration
};
buildYup(jsonSchema, config);
strict
default
nullable
required
notRequired
oneOf
(enum
, anyOf
)notOneOf
when
NEWisType
NEWnullable
(isNullable
) NEWensure
compact
items
(of
)maxItems
(max
)minItems
(min
)itemsOf
(of
) NEWNo keys
maxDate
(max
)minDate
(min
)integer
moreThan
(exclusiveMinimum
)lessThan
(exclusiveMaximum
)positive
negative
min
(minimum
)max
(maximum
)truncate
round
camelCase
constantCase
noUnknown
(propertyNames
)minLength
(min
)maxLength
(max
)pattern
(matches
or regex
)email
(format: 'email'
)url
(format: 'url'
)lowercase
uppercase
trim
Basic support for when conditions as requested and outlined in this issue is now included.
Work will continue in the when-condition branch.
Sample schema using simple when
constraint:
const biggyJson = {
title: "biggy",
type: "object",
properties: {
isBig: {
type: "boolean"
},
count: {
type: "number",
when: {
isBig: {
is: true,
then: {
min: 5
}
}
}
}
}
};
Sample valid and invalid values with respect to biggyJson
schema
const bigJson = {
valid: {
isBig: true,
count: 5 // since isBig is set, must be >= 5
},
invalid: {
isBig: true,
count: 4 // since isBig is set, must be >= 5
}
};
Currently basic support is included in schema-to-yup@1.9.0
on npmjs
More advanced conditionals support will likely be included the next major release: 2.0
.
You are welcome to continue the effort to support more conditional schema logic by continuing on this branch and making PRs.
Support for if
then
and else
conditional JSON schema constraints can likely be added using an approach like the when
condition (perhaps by transalating to equivalent: when
, then
and otherwise
).
if
- This keyword's value MUST be a valid JSON Schemathen
- This keyword's value MUST be a valid JSON Schemaelse
- This keyword's value MUST be a valid JSON SchemaSee also json-schema-spec
You can now also override, extend or customize the when
condition logic by passing in your own factory method for the config object entry createWhenCondition
const myWhenConditionFactoryFn = (opts = {}) => {
const { type, key, value, when, schema, properties, config } = opts;
// ...
};
const config = {
createWhenCondition: myWhenConditionFactoryFn
};
const schema = buildYup(nameJsonSchema, config);
The best and easiest way to do this is to extend the WhenCondition
class which contains most of the necessary infrastructure you can further build on.
See the src/conditions/legacy
folder for the legacy 1.9.0
logic that works but has limited functionality.
Here a more complete example of the variations currently possible
{
"title": "Person",
"description": "A person",
"type": "object",
"properties": {
"name": {
"description": "Name of the person",
"type": "string",
"required": true,
"matches": "[a-zA-Z- ]+",
"mix": 3,
"maxLength": 40,
},
"age": {
"description": "Age of person",
"type": "integer",
"moreThan": 0,
"max": 130,
"default": 32,
"required": false,
"nullable": true
},
"birthday": {
"type": "date",
"min": "1-1-1900",
"maxDate": "1-1-2015"
},
"married": {
"type": "boolean"
},
"boss": {
"type": "object",
"noUnknown": [
"name"
],
"properties": {
"name": {
"type": "string",
"notOneOf": ["Dr. evil", "bad ass"]
}
}
},
"colleagues": {
"type": "array",
"items": {
"type": "object",
"propertyNames": [
"name"
],
"properties": {
"name": {
"type": "string"
}
}
}
},
"programming": {
"type": "object",
"properties": {
"languages": {
"type": "array",
"of": {
"type": "string",
"enum": ["javascript", "java", "C#"]
},
"min": 1,
"max": 3
}
}
}
}
}
}
Nested object schema properties are now finally fully supported.
See test/types/object/complex-schema.test.js
This library now also supports non JSON schema models. See the types/defaults
mappings.
types/defaults/json-schema.js
module.exports {
getProps: obj => obj.properties,
getType: obj => obj.type,
getName: obj => obj.name || obj.title,
getConstraints: obj => obj,
isString: obj => obj.type === "string",
isArray: obj => obj.type === "array",
isInteger: obj => obj.type === "integer",
isBoolean: obj => obj.type === "boolean",
hasDateFormat: obj => ["date", "date-time"].find(t => t === obj.format),
isDate: obj => obj.type === "string" && defaults.hasDateFormat(obj.format),
isNumber: obj => obj.type === "number" || defaults.isInteger(obj.type),
isObject: obj => obj.type === "object",
isRequired: obj => obj.required
};
To support another model, such as GraphQL schema (type definitions) via graphSchemaToJson
Person:
{
Person: {
name: 'Person',
fields: {
name: {
type: 'String',
directives: {
constraints: {
minLength: 2
}
},
isNullable: false,
isList: false
}
}
directives: {},
type: 'Object',
implements: []
}
}
Create a map of methods to match your model layout:
const typeDefConf = {
getProps: obj => obj.fields,
getType: obj => obj.type,
getName: obj => obj.name,
getConstraints: obj => (obj.directives || {}).constraints || {},
isString: obj => obj.type === "String",
isArray: obj => obj.isList,
isInteger: obj => obj.type === "Int",
isBoolean: obj => obj.type === "Boolean",
isDate: obj => obj.type === "Date" || obj.directives.date,
isNumber: obj => obj.type === "Int" || obj.type === "Float",
isObject: obj => obj.type === "Object",
isRequired: obj => !obj.isNullable
};
Please note that getConstraints
can be used to control where the constraints of the field will be taken from (depending on the type of model/schema or your preference).
Pass overrides to match your model in config
as follows:
const schema = buildYup(nameJsonSchema, { ...typeDefConf, log: true });
The type definition mappings above are already built-in and available.
To switch the schema type, pass schemaType
in config as follows.
const schema = buildYup(nameJsonSchema, { schemaType: "type-def", log: true });
Feel free to make PRs to make more common schema models conveniently available!
You can enable logging py passing a log
option in the config
argument. If set to true, it will by default assign the internal log function to console.log
const schema = buildYup(nameJsonSchema, { log: true });
You can also pass a log function in the log
option to handle log messages and an err
option with a custom error handler function.
See Custom errors in Node for how to design custom errors
class ValidationError extends Error {}
const schema = buildYup(nameJsonSchema, {
log: (name, msg) => console.log(`[${name}] ${msg}`)
err: (msg) => {
console.error(`[${name}] ERROR: ${msg}`
throw new ValidationError(msg)
})
});
You can supply a createYupSchemaEntry
function as an entry in the config
object.
This function will then be used to build each Yup Schema entry in the Yup Schema being built.
Use the Yup Type classes such as types.YupArray
to act as building blocks or create your own custom logic as you see fit.
const { YupSchemaEntry, buildYup, types } = require("schema-to-yup");
class CustomYupArray extends types.YupArray {
// ...
}
class CustomYupSchemaEntry extends YupSchemaEntry {
// ...
}
function createYupSchemaEntry(key, value, config) {
const builder = new CustomYupSchemaEntryBuilder(key, value, config);
builder.types.array = config => createYupArray(config);
return builder.toEntry();
}
// use some localized error messages
const messages = i18n.locale(LOCALE);
const yupSchema = buildYup(json, {
createYupSchemaEntry,
messages
});
You can use extendYupApi
to extend the Yup API with extra validation methods:
const validator = require("validator");
const { extendYupApi } = require("schema-to-yup/validator-bridge");
// by default extends with string format validation methods of validator
// See https://www.npmjs.com/package/validator
extendYupApi({ validator });
You can optionally pass in a custom validator
and a constraints map of your choice.
You can either extend the default constraints or override them with your own map.
PS: Check out src/validator-bridge
for more many options for fine control
const myValidator = new MyValidator();
const constraints = ["creditCard", "currency", { name: "hash", opts: "algo" }];
extendYupApi({ validator: myValidator, override: true, constraints });
const { buildYup } = require("schema-to-yup");
// type def sample schema, using credit-card format validator
const schema = {
name: "BankAccount",
fields: {
accountNumber: {
type: "String",
format: "credit-card"
}
}
};
// opt in to use generic string format validation, via format: true config option
const yupSchema = buildYup(schema, { format: true, schemaType: "type-def" });
// ...do your validation
const valid = await yupSchema.isValid({
accountNumber: "123-4567-1828-2929"
});
Now the bridge includes tests. Seems to work ;)
You can sublass YupBuilder
or any of the internal classes to create your own custom infrastructure to suit your particular needs, expand with support for extra features etc.
const { YupBuilder } = require("schema-to-yup");
class MyYupBuilder extends YupBuilder {
// ... custom overrides etc
}
const builder = new MyYupBuilder(schema, config);
const { yupSchema } = builder;
// ...
You can pass an errMessages
object in the optional config
object argument with key mappings for your custom validation error messages.
Internally the validator error messages are resolved via an instance of the ErrorMessageHandler
calling the valErrMessage
method.
notOneOf() {
const {not, notOneOf} = this.value
const $oneOf = notOneOf || (not && (not.enum || not.oneOf))
$oneOf && this
.base
.notOneOf($oneOf, this.valErrMessage('notOneOf'))
return this
}
We recommend you subclass the existing ErrorMessageHandler
as follow
class MyErrorMessageHandler extends ErrorMessageHandler {
// ...
}
const createErrorMessageHandler = (typeHandler, config = {}) => {
return new MyErrorMessageHandler(typeHandler, config);
};
You could f.ex override errMessageFor
as follows, using the type
errMessageFor(name) {
return this.propertyErrMessageFor(name) || this.genericErrMessageFor(name)
}
propertyErrMessageFor(name) {
const { errMessages, key } = this;
const errMsg = errMessages[key];
if (!errMsg) return
return errMsg[name]
}
genericErrMessageFor(name) {
const { errMessages, type } = this;
const genricErrMsgMap = errMessages['_generic_'];
const fullName = `${type}.${name}`
return genricErrMsgMap[fullName] || genricErrMsgMap[name];
}
Then pass in your factory function in config
as follows.
const config = {
createErrorMessageHandler
};
You can pass the errMessages
as follows. Note that you can define error messages specific to a property such as emailAdr.format
and generic messages prefixed with $
such as $email
.
Note: This convention might well change in future releases.
let config = {
errMessages: {
emailAdr: {
// note: would also work with email as the key
format: "emailAdr must be a valid email"
},
// generic fallback message for any email format validation
// note: if not present uses yup default validation message
$email: "Email format incorrect"
}
};
The key entries can be either a function, taking a value
argument or a static string.
Here are some of the defaults that you can override as needed.
export const errValKeys = [
"oneOf",
"enum",
"required",
"notRequired",
"minDate",
"min",
"maxDate",
"max",
"trim",
"lowercase",
"uppercase",
"email",
"url",
"minLength",
"maxLength",
"pattern",
"matches",
"regex",
"integer",
"positive",
"minimum",
"maximum"
];
export const defaults = {
errMessages: (keys = errValKeys) =>
keys.reduce((acc, key) => {
const fn = ({ key, value }) =>
`${key}: invalid for ${value.name || value.title}`;
acc[key] = fn;
return acc;
}, {})
};
const { buildYup, types } = require("schema-to-yup");
const { defaults } = types;
const myErrMessages = require("./err-messages");
const valKeys = ["lowercase", "integer"];
// by default Yup built-in validation error messages will be used if not overridden here
const errMessages = {
...defaults.errMessages(valKeys),
myErrMessages
};
const yupSchema = buildYup(json, {
errMessages
});
See the number
type for the current best practice to add type constraints.
For simple cases: use addConstraint
from the superclass YupMixed
required() {
return this.addConstraint("required");
}
For types with several of these, we should map through a list or map to add them all.
strict() {
return this.addValueConstraint("strict");
}
required() {
return this.addConstraint("required");
}
notRequired() {
return this.addConstraint("notRequired");
}
Can be rewritten to use conventions, iterating a map:
addMappedConstraints() {
Object.keys(this.constraintsMap).map(key => {
const list = constraintsMap[key];
const fnName = key === 'value' ? 'addValueConstraint' : 'addConstraint'
list.map(this.[fnName]);
});
}
get constraintsMap() {
return {
simple: ["required", "notRequired", "nullable"],
value: ["default", "strict"]
};
}
For more complex contraints that:
You can create a separate Constraint
subclass, to offload and handle it all separately.
Here is a sample RangeConstraint
used by number.
class RangeConstraint extends NumericConstraint {
constructor(typer) {
super(typer);
}
get $map() {
return {
moreThan: ["exclusiveMinimum", "moreThan"],
lessThan: ["exclusiveMaximum", "lessThan"],
max: ["maximum", "max"],
min: ["minimum", "min"]
};
}
}
Instead of wrapping a Constraint
you can call it directly with a map
// this would be an instance such as YupNumber
// map equivalent to $map in the RangeConstraint
range() {
return createNumericConstraint(this, map);
}
For the core type constraint class (such as YupNumber
) you should now be able to simplify it to:
get enabled() {
return ["range", "posNeg", "integer"];
}
convert() {
this.enabled.map(name => this.processConstraint(name));
super.convert();
return this;
}
The following constraint classes are available for use:
NumericConstraint
StringConstraint
RegExpConstraint
DateConstraint
Currently only YupNumber
has been (partly) refactored to take advantage of this new infrastructure. Please help refactor the rest!
YupNumber
also has the most unit test coverage, used to test the current infrastructure!
The library JSON Schema model builder is a powerful toolset to build a framework to create any kind of output model from a JSON schema.
If you enjoy this declarative/generator approach, try it out!
Uses jest for unit testing.
NumericConstraint
)Current development is taking place on refactoring branch.
On his branch:
If you would like to further improved this library or add support for more validators than Yup, please help on this branch. Cheers!
Please feel free to come with ideas and suggestions on how to further improve this library.
2018 Kristian Mandrup (CTO@Tecla5)
MIT
FAQs
Build a Yup schema object to validate models from a domain model schema (JSON or GraphQL)
The npm package schema-to-yup receives a total of 49,685 weekly downloads. As such, schema-to-yup popularity was classified as popular.
We found that schema-to-yup demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Cloudflare has launched a setup wizard allowing users to easily create and manage a security.txt file for vulnerability disclosure on their websites.
Security News
The Socket Research team breaks down a malicious npm package targeting the legitimate DOMPurify library. It uses obfuscated code to hide that it is exfiltrating browser and crypto wallet data.
Security News
ENISA’s 2024 report highlights the EU’s top cybersecurity threats, including rising DDoS attacks, ransomware, supply chain vulnerabilities, and weaponized AI.