Implement support for JSON Schema validation
This commit is contained in:
parent
b30aac082b
commit
98f7cacb55
|
@ -72,6 +72,15 @@
|
|||
"resolve": "^1.11.0"
|
||||
}
|
||||
},
|
||||
"@rollup/plugin-json": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-4.1.0.tgz",
|
||||
"integrity": "sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@rollup/pluginutils": "^3.0.8"
|
||||
}
|
||||
},
|
||||
"@rollup/plugin-node-resolve": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-8.4.0.tgz",
|
||||
|
@ -134,6 +143,22 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"ace-builds": {
|
||||
"version": "1.4.12",
|
||||
"resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.4.12.tgz",
|
||||
"integrity": "sha512-G+chJctFPiiLGvs3+/Mly3apXTcfgE45dT5yp12BcWZ1kUs+gm0qd3/fv4gsz6fVag4mM0moHVpjHDIgph6Psg=="
|
||||
},
|
||||
"ajv": {
|
||||
"version": "6.12.3",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz",
|
||||
"integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==",
|
||||
"requires": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
}
|
||||
},
|
||||
"ansi-colors": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
|
||||
|
@ -229,6 +254,12 @@
|
|||
"integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
|
||||
"dev": true
|
||||
},
|
||||
"btoa": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
|
||||
"integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
|
||||
"dev": true
|
||||
},
|
||||
"buffer-from": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
|
||||
|
@ -491,6 +522,16 @@
|
|||
"integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==",
|
||||
"dev": true
|
||||
},
|
||||
"fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||
},
|
||||
"fast-json-stable-stringify": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
|
@ -804,6 +845,11 @@
|
|||
"esprima": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
|
||||
},
|
||||
"kleur": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
|
||||
|
@ -899,6 +945,12 @@
|
|||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
||||
"dev": true
|
||||
},
|
||||
"mocha": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mocha/-/mocha-8.0.1.tgz",
|
||||
|
@ -1083,6 +1135,11 @@
|
|||
"iterate-value": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"punycode": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
|
||||
},
|
||||
"randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
|
@ -1422,6 +1479,14 @@
|
|||
"integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==",
|
||||
"dev": true
|
||||
},
|
||||
"uri-js": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
|
||||
"integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
|
||||
"requires": {
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
"build": "rollup -c",
|
||||
"dev": "rollup -c -w",
|
||||
"start": "sirv public",
|
||||
"test": "mocha ./src/**/*.test.js",
|
||||
"prepare": "node tools/fix-lodash-es.cjs"
|
||||
"test": "mocha ./src/**/*.test.js ./src/**/*.test.mjs",
|
||||
"prepare": "node tools/fixLodashEs.cjs && node tools/generateAceWorker.mjs && node tools/generateAjvDrafts.mjs"
|
||||
},
|
||||
"license": "(MIT OR Apache-2.0)",
|
||||
"repository": {
|
||||
|
@ -18,13 +18,18 @@
|
|||
"dependencies": {
|
||||
"@fortawesome/free-regular-svg-icons": "5.14.0",
|
||||
"@fortawesome/free-solid-svg-icons": "5.14.0",
|
||||
"ace-builds": "1.4.12",
|
||||
"ajv": "6.12.3",
|
||||
"classnames": "2.2.6",
|
||||
"lodash-es": "4.17.15",
|
||||
"svelte-awesome": "2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "14.0.0",
|
||||
"@rollup/plugin-json": "4.1.0",
|
||||
"@rollup/plugin-node-resolve": "8.4.0",
|
||||
"btoa": "1.2.1",
|
||||
"mkdirp": "1.0.4",
|
||||
"mocha": "8.0.1",
|
||||
"rollup": "2.23.0",
|
||||
"rollup-plugin-livereload": "1.3.0",
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
|
||||
<title>JSONEditor | JSON schema validation</title>
|
||||
|
||||
<style type="text/css">
|
||||
body {
|
||||
width: 600px;
|
||||
font: 11pt sans-serif;
|
||||
}
|
||||
#jsoneditor {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
/* custom bold styling for non-default JSON schema values */
|
||||
.jsoneditor-is-not-default {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>JSON schema validation</h1>
|
||||
<p>
|
||||
This example demonstrates JSON schema validation. The JSON object in this example must contain properties like <code>firstName</code> and <code>lastName</code>, can can optionally have a property <code>age</code> which must be a positive integer.
|
||||
</p>
|
||||
<p>
|
||||
See <a href="http://json-schema.org/" target="_blank">http://json-schema.org/</a> for more information.
|
||||
</p>
|
||||
|
||||
<div id="jsoneditor"></div>
|
||||
|
||||
<script type="module">
|
||||
import jsoneditor, { createAjvValidator } from "../dist/es/jsoneditor.js"
|
||||
|
||||
const schema = {
|
||||
"title": "Employee",
|
||||
"description": "Object containing employee details",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"firstName": {
|
||||
"title": "First Name",
|
||||
"description": "The given name.",
|
||||
"examples": [
|
||||
"John"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"lastName": {
|
||||
"title": "Last Name",
|
||||
"description": "The family name.",
|
||||
"examples": [
|
||||
"Smith"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"gender": {
|
||||
"title": "Gender",
|
||||
"enum": ["male", "female"]
|
||||
},
|
||||
"availableToHire": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"age": {
|
||||
"description": "Age in years",
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"examples": [28, 32]
|
||||
},
|
||||
"job": {
|
||||
"$ref": "job"
|
||||
}
|
||||
},
|
||||
"required": ["firstName", "lastName"]
|
||||
}
|
||||
|
||||
const schemaRefs = {
|
||||
job: {
|
||||
"title": "Job description",
|
||||
"type": "object",
|
||||
"required": ["address"],
|
||||
"properties": {
|
||||
"company": {
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"ACME",
|
||||
"Dexter Industries"
|
||||
]
|
||||
},
|
||||
"role": {
|
||||
"description": "Job title.",
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"Human Resources Coordinator",
|
||||
"Software Developer"
|
||||
],
|
||||
"default": "Software Developer"
|
||||
},
|
||||
"address": {
|
||||
"type": "string"
|
||||
},
|
||||
"salary": {
|
||||
"type": "number",
|
||||
"minimum": 120,
|
||||
"examples": [100, 110, 120]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const doc = {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
gender: null,
|
||||
age: "28",
|
||||
availableToHire: true,
|
||||
job: {
|
||||
company: 'freelance',
|
||||
role: 'developer',
|
||||
salary: 100
|
||||
}
|
||||
}
|
||||
|
||||
// create the editor
|
||||
const editor = jsoneditor({
|
||||
target: document.getElementById('jsoneditor'),
|
||||
props: {
|
||||
doc,
|
||||
onChangeJson: doc => console.log('onChangeJson', doc),
|
||||
validate: createAjvValidator(schema, schemaRefs)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,6 +1,7 @@
|
|||
import svelte from 'rollup-plugin-svelte';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import json from '@rollup/plugin-json';
|
||||
import livereload from 'rollup-plugin-livereload';
|
||||
import { terser } from 'rollup-plugin-terser';
|
||||
import autoPreprocess from 'svelte-preprocess';
|
||||
|
@ -39,6 +40,7 @@ export default {
|
|||
dedupe: ['svelte']
|
||||
}),
|
||||
commonjs(),
|
||||
json(),
|
||||
|
||||
// In dev mode, call `npm run start` once
|
||||
// the bundle has been generated
|
||||
|
|
|
@ -38,13 +38,17 @@
|
|||
let divContents
|
||||
let domHiddenInput
|
||||
|
||||
export let validate = () => []
|
||||
export let validate = null
|
||||
export let onChangeJson = () => {}
|
||||
|
||||
export function setValidator (newValidate) {
|
||||
validate = newValidate
|
||||
}
|
||||
|
||||
export function getValidator () {
|
||||
return validate
|
||||
}
|
||||
|
||||
export let doc = {}
|
||||
let state = undefined
|
||||
|
||||
|
@ -52,7 +56,7 @@
|
|||
let clipboard = null
|
||||
|
||||
$: state = syncState(doc, state, [], (path) => path.length < 1)
|
||||
$: validationErrorsList = validate(doc)
|
||||
$: validationErrorsList = validate ? validate(doc) : []
|
||||
$: validationErrors = mapValidationErrors(validationErrorsList)
|
||||
|
||||
let showSearch = false
|
||||
|
|
|
@ -151,7 +151,6 @@
|
|||
margin: 0 5px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
|
||||
&:hover {
|
||||
background: lighten($light-gray, 5%);
|
||||
|
|
|
@ -3,3 +3,5 @@ import JSONEditor from './components/JSONEditor.svelte'
|
|||
export default function jsoneditor (config) {
|
||||
return new JSONEditor(config)
|
||||
}
|
||||
|
||||
export { createAjvValidator } from './plugins/createAjvValidator.mjs'
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
import Ajv from 'ajv'
|
||||
import { parseJSONPointer } from '../utils/jsonPointer.js'
|
||||
import { draft04 } from '../generated/ajv/draft04.mjs'
|
||||
import { draft06 } from '../generated/ajv/draft06.mjs'
|
||||
|
||||
/**
|
||||
* Create a JSON Schema validator powered by Ajv.
|
||||
* @param {JSON} schema
|
||||
* @param {Object} [schemaRefs=undefined] An object containing JSON Schema references
|
||||
*/
|
||||
export function createAjvValidator (schema, schemaRefs) {
|
||||
const ajv = Ajv({
|
||||
allErrors: true,
|
||||
verbose: true,
|
||||
schemaId: 'auto',
|
||||
jsonPointers: true,
|
||||
$data: true
|
||||
})
|
||||
|
||||
// support both draft-04 and draft-06 alongside the latest draft-07
|
||||
ajv.addMetaSchema(draft04) // FIXME
|
||||
ajv.addMetaSchema(draft06) // FIXME
|
||||
|
||||
if (schemaRefs) {
|
||||
Object.keys(schemaRefs).forEach(ref => {
|
||||
ajv.addSchema(schemaRefs[ref], ref)
|
||||
})
|
||||
}
|
||||
|
||||
const validateAjv = ajv.compile(schema)
|
||||
|
||||
return function validate (doc) {
|
||||
validateAjv(doc)
|
||||
const ajvErrors = validateAjv.errors
|
||||
|
||||
return ajvErrors
|
||||
.map(improveAjvError)
|
||||
.map(normalizeAjvError)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} ajvError
|
||||
* @return {ValidationError}
|
||||
*/
|
||||
function normalizeAjvError (ajvError) {
|
||||
return {
|
||||
path: parseJSONPointer(ajvError.dataPath),
|
||||
message: ajvError.message
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Improve the error message of a JSON schema error,
|
||||
* for example list the available values of an enum.
|
||||
*
|
||||
* @param {Object} ajvError
|
||||
* @return {Object} Returns the error with improved message
|
||||
*/
|
||||
function improveAjvError (ajvError) {
|
||||
if (ajvError.keyword === 'enum' && Array.isArray(ajvError.schema)) {
|
||||
let enums = ajvError.schema
|
||||
if (enums) {
|
||||
enums = enums.map(value => JSON.stringify(value))
|
||||
|
||||
if (enums.length > 5) {
|
||||
const more = ['(' + (enums.length - 5) + ' more...)']
|
||||
enums = enums.slice(0, 5)
|
||||
enums.push(more)
|
||||
}
|
||||
ajvError.message = 'should be equal to one of: ' + enums.join(', ')
|
||||
}
|
||||
}
|
||||
|
||||
if (ajvError.keyword === 'additionalProperties') {
|
||||
ajvError.message = 'should NOT have additional property: ' + ajvError.params.additionalProperty
|
||||
}
|
||||
|
||||
return ajvError
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
import assert from "assert"
|
||||
import { createAjvValidator } from './createAjvValidator.mjs'
|
||||
|
||||
const schema = {
|
||||
"title": "Employee",
|
||||
"description": "Object containing employee details",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"firstName": {
|
||||
"title": "First Name",
|
||||
"description": "The given name.",
|
||||
"examples": [
|
||||
"John"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"lastName": {
|
||||
"title": "Last Name",
|
||||
"description": "The family name.",
|
||||
"examples": [
|
||||
"Smith"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"gender": {
|
||||
"title": "Gender",
|
||||
"enum": ["male", "female"]
|
||||
},
|
||||
"availableToHire": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"age": {
|
||||
"description": "Age in years",
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"examples": [28, 32]
|
||||
},
|
||||
"job": {
|
||||
"$ref": "job"
|
||||
}
|
||||
},
|
||||
"required": ["firstName", "lastName"]
|
||||
}
|
||||
|
||||
const schemaRefs = {
|
||||
job: {
|
||||
"title": "Job description",
|
||||
"type": "object",
|
||||
"required": ["address"],
|
||||
"properties": {
|
||||
"company": {
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"ACME",
|
||||
"Dexter Industries"
|
||||
]
|
||||
},
|
||||
"role": {
|
||||
"description": "Job title.",
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"Human Resources Coordinator",
|
||||
"Software Developer"
|
||||
],
|
||||
"default": "Software Developer"
|
||||
},
|
||||
"address": {
|
||||
"type": "string"
|
||||
},
|
||||
"salary": {
|
||||
"type": "number",
|
||||
"minimum": 120,
|
||||
"examples": [100, 110, 120]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('createAjvValidator', () => {
|
||||
it('should create a validate function', () => {
|
||||
const validate = createAjvValidator(schema, schemaRefs)
|
||||
|
||||
const invalidDoc = {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
gender: null,
|
||||
age: "28",
|
||||
availableToHire: true,
|
||||
job: {
|
||||
company: 'freelance',
|
||||
role: 'developer',
|
||||
salary: 100
|
||||
}
|
||||
}
|
||||
|
||||
assert.deepStrictEqual(validate(invalidDoc), [
|
||||
{ path: ['gender'], message: 'should be equal to one of: \"male\", \"female\"' },
|
||||
{ path: ['age'], message: 'should be integer' },
|
||||
{ path: ['job'], message: 'should have required property \'address\'' },
|
||||
{ path: ['job', 'salary'], message: 'should be >= 120' }
|
||||
])
|
||||
})
|
||||
|
||||
// TODO: test support for draft04, draft-06, draft-07
|
||||
})
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
$font-family: consolas, monaco, "lucida console", "courier new", "dejavu sans mono", "droid sans mono", courier, monospace, sans-serif;
|
||||
$font-size: 13px;
|
||||
$font-size: 14px;
|
||||
$font-size-small: 11px;
|
||||
$font-family-menu: arial, "sans-serif";
|
||||
$font-size-icon: 16px;
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import { readFileSync, writeFileSync } from 'fs'
|
||||
import mkdirp from 'mkdirp'
|
||||
import path from 'path'
|
||||
|
||||
const outputFolder = './src/generated/ajv'
|
||||
const jsonDrafts = [
|
||||
{
|
||||
name: 'draft04',
|
||||
path: './node_modules/ajv/lib/refs/json-schema-draft-04.json'
|
||||
},
|
||||
{
|
||||
name: 'draft06',
|
||||
path: './node_modules/ajv/lib/refs/json-schema-draft-06.json'
|
||||
}
|
||||
]
|
||||
|
||||
mkdirp.sync(outputFolder)
|
||||
|
||||
// Create an embedded version of the json drafts for Ajv: a data url
|
||||
jsonDrafts.forEach(jsonDraft => {
|
||||
const json = String(readFileSync(jsonDraft.path))
|
||||
const outputFile = path.join(outputFolder, jsonDraft.name + '.mjs')
|
||||
|
||||
writeFileSync(outputFile, `export const ${jsonDraft.name} = ${json}\n`)
|
||||
})
|
Loading…
Reference in New Issue