Security News
Research
Data Theft Repackaged: A Case Study in Malicious Wrapper Packages on npm
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
fluent-json-schema
Advanced tools
A fluent API to generate JSON schemas (draft-07) for Node.js and browser. Framework agnostic.
npm i fluent-json-schema
or
yarn add fluent-json-schema
const S = require('fluent-json-schema')
const ROLES = {
ADMIN: 'ADMIN',
USER: 'USER',
}
const schema = S.object()
.id('http://foo/user')
.title('My First Fluent JSON Schema')
.description('A simple user')
.prop('email', S.string().format(S.FORMATS.EMAIL).required())
.prop('password', S.string().minLength(8).required())
.prop('role', S.string().enum(Object.values(ROLES)).default(ROLES.USER))
.prop(
'birthday',
S.raw({ type: 'string', format: 'date', formatMaximum: '2020-01-01' }) // formatMaximum is an AJV custom keywords
)
.definition(
'address',
S.object()
.id('#address')
.prop('line1', S.anyOf([S.string(), S.null()])) // JSON Schema nullable
.prop('line2', S.string().raw({ nullable: true })) // Open API / Swagger nullable
.prop('country', S.string())
.prop('city', S.string())
.prop('zipcode', S.string())
.required(['line1', 'country', 'city', 'zipcode'])
)
.prop('address', S.ref('#address'))
console.log(JSON.stringify(schema.valueOf(), undefined, 2))
Schema generated:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"address": {
"type": "object",
"$id": "#address",
"properties": {
"line1": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"line2": {
"type": "string",
"nullable": true
},
"country": {
"type": "string"
},
"city": {
"type": "string"
},
"zipcode": {
"type": "string"
}
},
"required": ["line1", "country", "city", "zipcode"]
}
},
"type": "object",
"$id": "http://foo/user",
"title": "My First Fluent JSON Schema",
"description": "A simple user",
"properties": {
"email": {
"type": "string",
"format": "email"
},
"password": {
"type": "string",
"minLength": 8
},
"birthday": {
"type": "string",
"format": "date",
"formatMaximum": "2020-01-01"
},
"role": {
"type": "string",
"enum": ["ADMIN", "USER"],
"default": "USER"
},
"address": {
"$ref": "#address"
}
},
"required": ["email", "password"]
}
With "esModuleInterop": true
activated in the tsconfig.json
:
import S from 'fluent-json-schema'
const schema = S.object()
.prop('foo', S.string())
.prop('bar', S.number())
.valueOf()
With "esModuleInterop": false
in the tsconfig.json
:
import * as S from 'fluent-json-schema'
const schema = S.object()
.prop('foo', S.string())
.prop('bar', S.number())
.valueOf()
A named export is also available to work with native ESM modules:
import { S } from 'fluent-json-schema'
const schema = S.object()
.prop('foo', S.string())
.prop('bar', S.number())
.valueOf()
Fluent schema does not validate a JSON schema. However, many libraries can do that for you. Below a few examples using AJV:
npm i ajv
or
yarn add ajv
Snippet:
const ajv = new Ajv({ allErrors: true })
const validate = ajv.compile(schema.valueOf())
let user = {}
let valid = validate(user)
console.log({ valid }) //=> {valid: false}
console.log(validate.errors) //=> {valid: false}
Output:
{valid: false}
errors: [
{
keyword: 'required',
dataPath: '',
schemaPath: '#/required',
params: { missingProperty: 'email' },
message: "should have required property 'email'",
},
{
keyword: 'required',
dataPath: '',
schemaPath: '#/required',
params: { missingProperty: 'password' },
message: "should have required property 'password'",
},
]
Snippet:
user = { email: 'test', password: 'password' }
valid = validate(user)
console.log({ valid })
console.log(validate.errors)
Output:
{valid: false}
errors:
[ { keyword: 'format',
dataPath: '.email',
schemaPath: '#/properties/email/format',
params: { format: 'email' },
message: 'should match format "email"' } ]
Snippet:
user = { email: 'test@foo.com', password: 'password' }
valid = validate(user)
console.log({ valid })
console.log('errors:', validate.errors)
Output:
{valid: false}
errors: [ { keyword: 'required',
dataPath: '.address',
schemaPath: '#definitions/address/required',
params: { missingProperty: 'country' },
message: 'should have required property \'country\'' },
{ keyword: 'required',
dataPath: '.address',
schemaPath: '#definitions/address/required',
params: { missingProperty: 'city' },
message: 'should have required property \'city\'' },
{ keyword: 'required',
dataPath: '.address',
schemaPath: '#definitions/address/required',
params: { missingProperty: 'zipcoce' },
message: 'should have required property \'zipcode\'' } ]
Snippet:
user = { email: 'test@foo.com', password: 'password' }
valid = validate(user)
console.log({ valid })
Output:
{valid: true}
Normally inheritance with JSON Schema is achieved with allOf
. However when .additionalProperties(false)
is used the validator won't
understand which properties come from the base schema. S.extend
creates a schema merging the base into the new one so
that the validator knows all the properties because it is evaluating only a single schema.
For example, in a CRUD API POST /users
could use the userBaseSchema
rather than GET /users
or PATCH /users
use the userSchema
which contains the id
, createdAt
and updatedAt
generated server side.
const S = require('fluent-json-schema')
const userBaseSchema = S.object()
.additionalProperties(false)
.prop('username', S.string())
.prop('password', S.string())
const userSchema = S.object()
.prop('id', S.string().format('uuid'))
.prop('createdAt', S.string().format('time'))
.prop('updatedAt', S.string().format('time'))
.extend(userBaseSchema)
console.log(userSchema)
In addition to extending schemas, it is also possible to reduce them into smaller schemas. This comes in handy when you have a large Fluent Schema, and would like to re-use some of its properties.
Select only properties you want to keep.
const S = require('fluent-json-schema')
const userSchema = S.object()
.prop('username', S.string())
.prop('password', S.string())
.prop('id', S.string().format('uuid'))
.prop('createdAt', S.string().format('time'))
.prop('updatedAt', S.string().format('time'))
const loginSchema = userSchema.only(['username', 'password'])
Or remove properties you dont want to keep.
const S = require('fluent-json-schema')
const personSchema = S.object()
.prop('name', S.string())
.prop('age', S.number())
.prop('id', S.string().format('uuid'))
.prop('createdAt', S.string().format('time'))
.prop('updatedAt', S.string().format('time'))
const bodySchema = personSchema.without(['createdAt', 'updatedAt'])
Every Fluent Schema object contains a boolean isFluentSchema
. In this way, you can write your own utilities that understands the Fluent Schema API and improve the user experience of your tool.
const S = require('fluent-json-schema')
const schema = S.object().prop('foo', S.string()).prop('bar', S.number())
console.log(schema.isFluentSchema) // true
Thanks to Matteo Collina for pushing me to implement this utility! 🙏
Licensed under MIT.
FAQs
JSON Schema fluent API
We found that fluent-json-schema demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 10 open source maintainers 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
Research
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Research
Security News
Attackers used a malicious npm package typosquatting a popular ESLint plugin to steal sensitive data, execute commands, and exploit developer systems.
Security News
The Ultralytics' PyPI Package was compromised four times in one weekend through GitHub Actions cache poisoning and failure to rotate previously compromised API tokens.