Implemented support for custom validation

This commit is contained in:
jos 2018-08-15 10:48:51 +02:00
parent ed81af02ec
commit 8bb2962505
7 changed files with 271 additions and 12 deletions

View File

@ -3,6 +3,11 @@
https://github.com/josdejong/jsoneditor
## not yet released, version 5.23.0
- Implemented support for custom validation using a new `onValidate` callback.
## 2018-08-13, version 5.22.0
- Implemented `onEvent` callback triggered when an event occurs in a JSON

View File

@ -89,6 +89,31 @@ Constructs a new JSONEditor.
Set a callback function triggered right after the mode is changed by the user. Only applicable when
the mode can be changed by the user (i.e. when option `modes` is set).
- `{function} onValidate(json)`
Set a callback function for custom validation. Available in all modes.
On a change of the JSON, the callback function is invoked with the changed data. The function should return
an array with errors (or an empty array when the document is valid). The returned errors must have the following
structure: `{path: Array.<string | number>, message: string}`. Example:
```js
var options = {
onValidate: function (json) {
var errors = [];
if (json && json.customer && !json.customer.address) {
errors.push({
path: ['customer'],
message: 'Required property "address" missing.'
});
}
return errors;
}
}
```
- `{boolean} escapeUnicode`
If true, unicode characters are escaped and displayed as their hexadecimal code (like `\u260E`) instead of of the character itself (like `☎`). False by default.

View File

@ -0,0 +1,110 @@
<!DOCTYPE HTML>
<html>
<head>
<title>JSONEditor | Custom validation</title>
<link href="../dist/jsoneditor.css" rel="stylesheet" type="text/css">
<script src="../dist/jsoneditor.js"></script>
<style type="text/css">
body {
width: 600px;
font: 11pt sans-serif;
}
#jsoneditor {
width: 100%;
height: 500px;
}
</style>
</head>
<body>
<h1>Custom validation</h1>
<p>
This example demonstrates how to run custom validation on a JSON object.
The validation is available in all modes.
</p>
<div id="jsoneditor"></div>
<script>
var json = {
team: [
{
name: 'Joe',
age: 17
},
{
name: 'Sarah',
age: 13
},
{
name: 'Jack'
}
]
};
var options = {
mode: 'tree',
modes: ['code', 'text', 'tree'],
onValidate: function (json) {
// rules:
// - team, names, and ages must be filled in and be of correct type
// - a team must have 4 members
// - at lease one member of the team must be adult
var errors = [];
if (Array.isArray(json.team)) {
// check whether each team member has name and age filled in correctly
json.team.forEach(function (member, index) {
if (typeof member !== 'object') {
errors.push({ path: ['team', index], message: 'Member must be an object with properties "name" and "age"' })
}
if ('name' in member) {
if (typeof member.name !== 'string') {
errors.push({ path: ['team', index, 'name'], message: 'Name must be a string' })
}
}
else {
errors.push({ path: ['team', index], message: 'Required property "name"" missing' })
}
if ('age' in member) {
if (typeof member.age !== 'number') {
errors.push({ path: ['team', index, 'age'], message: 'Age must be a number' })
}
}
else {
errors.push({ path: ['team', index], message: 'Required property "age" missing' })
}
});
// check whether the team consists of exactly four members
if (json.team.length !== 4) {
errors.push({ path: ['team'], message: 'A team must have 4 members' })
}
// check whether there is at least one adult member in the team
var adults = json.team.filter(function (member) {
return member ? member.age >= 18 : false;
});
if (adults.length === 0) {
errors.push({ path: ['team'], message: 'A team must have at least one adult person (age >= 18)' })
}
}
else {
errors.push({ path: [], message: 'Required property "team" missing or not an Array' })
}
return errors;
}
};
// create the editor
var container = document.getElementById('jsoneditor');
var editor = new JSONEditor(container, options, json);
editor.expandAll();
</script>
</body>
</html>

View File

@ -153,9 +153,10 @@ JSONEditor.prototype.DEBOUNCE_INTERVAL = 150;
JSONEditor.VALID_OPTIONS = [
'ajv', 'schema', 'schemaRefs','templates',
'ace', 'theme','autocomplete',
'ace', 'theme', 'autocomplete',
'onChange', 'onChangeJSON', 'onChangeText',
'onEditable', 'onError', 'onEvent', 'onModeChange', 'onSelectionChange', 'onTextSelectionChange',
'onEditable', 'onError', 'onEvent', 'onModeChange', 'onValidate',
'onSelectionChange', 'onTextSelectionChange',
'escapeUnicode', 'history', 'search', 'mode', 'modes', 'name', 'indentation',
'sortObjectKeys', 'navigationBar', 'statusBar', 'languages', 'language'
];

View File

@ -720,12 +720,49 @@ textmode.validate = function () {
}
// only validate the JSON when parsing the JSON succeeded
if (doValidate && this.validateSchema) {
var valid = this.validateSchema(json);
if (!valid) {
errors = this.validateSchema.errors.map(function (error) {
return util.improveSchemaError(error);
});
if (doValidate) {
// execute JSON schema validation (ajv)
if (this.validateSchema) {
var valid = this.validateSchema(json);
if (!valid) {
errors = this.validateSchema.errors.map(function (error) {
return util.improveSchemaError(error);
});
}
}
// execute custom validation
if (this.options.onValidate) {
try {
var customValidationPathErrors = this.options.onValidate(json);
if (Array.isArray(customValidationPathErrors)) {
var customValidationErrors = customValidationPathErrors
.filter(function (error) {
var valid = util.isValidValidationError(error);
if (!valid) {
console.warn('Ignoring a custom validation error with invalid structure. ' +
'Expected structure: {path: [...], message: "..."}. ' +
'Actual error:', error);
}
return valid;
})
.map(function (error) {
// change data structure into the structure matching the JSON schema errors
return {
dataPath: util.stringifyPath(error.path),
message: error.message
}
});
errors = errors.concat(customValidationErrors);
}
}
catch (err) {
console.error(err);
}
}
}
@ -736,7 +773,7 @@ textmode.validate = function () {
errors.reduce(function(acc, curr) {
if(acc.indexOf(curr.dataPath) === -1) {
acc.push(curr.dataPath);
};
}
return acc;
}, errorPaths);
var errorLocations = util.getPositionForPath(jsonText, errorPaths);

View File

@ -524,13 +524,15 @@ treemode.validate = function () {
return;
}
var json = root.getValue();
// check for duplicate keys
var duplicateErrors = root.validate();
// validate the JSON
// execute JSON schema validation
var schemaErrors = [];
if (this.validateSchema) {
var valid = this.validateSchema(root.getValue());
var valid = this.validateSchema(json);
if (!valid) {
// apply all new errors
schemaErrors = this.validateSchema.errors
@ -549,7 +551,10 @@ treemode.validate = function () {
}
}
var errorNodes = duplicateErrors.concat(schemaErrors);
// execute custom validation
var customValidationErrors = this._validateCustom(json);
var errorNodes = duplicateErrors.concat(schemaErrors, customValidationErrors);
var parentPairs = errorNodes
.reduce(function (all, entry) {
return entry.node
@ -584,6 +589,58 @@ treemode.validate = function () {
});
};
/**
* execute custom validation if configured. Returns an empty array if not.
*/
treemode._validateCustom = function (json) {
try {
if (this.options.onValidate) {
var root = this.node;
var customValidationPathErrors = this.options.onValidate(json);
if (Array.isArray(customValidationPathErrors)) {
return customValidationPathErrors
.filter(function (error) {
var valid = util.isValidValidationError(error);
if (!valid) {
console.warn('Ignoring a custom validation error with invalid structure. ' +
'Expected structure: {path: [...], message: "..."}. ' +
'Actual error:', error);
}
return valid;
})
.map(function (error) {
var node;
try {
node = (error && error.path) ? root.findNodeByPath(error.path) : null
}
catch (err) {
// stay silent here, we throw a generic warning if no node is found
}
if (!node) {
console.warn('Ignoring validation error: node not found. Path:', error.path, 'Error:', error);
}
return {
node: node,
error: error
}
})
.filter(function (entry) {
return entry && entry.node && entry.error && entry.error.message
});
}
}
}
catch (err) {
console.error(err);
}
return [];
};
/**
* Refresh the rendered contents
*/

View File

@ -716,6 +716,19 @@ exports.parsePath = function parsePath(jsonPath) {
return [prop].concat(parsePath(remainder))
};
/**
* Stringify an array with a path in a JSON path like '.items[3].name'
* @param {Array.<string | number>} path
* @returns {string}
*/
exports.stringifyPath = function stringifyPath(path) {
return path
.map(function (p) {
return typeof p === 'number' ? ('[' + p + ']') : ('.' + p);
})
.join('');
};
/**
* Improve the error message of a JSON schema error
* @param {Object} error
@ -745,6 +758,17 @@ exports.improveSchemaError = function (error) {
return error;
};
/**
* Test whether a custom validation error has the correct structure
* @param {*} validationError The error to be checked.
* @returns {boolean} Returns true if the structure is ok, false otherwise
*/
exports.isValidValidationError = function (validationError) {
return typeof validationError === 'object' &&
Array.isArray(validationError.path) &&
typeof validationError.message === 'string';
};
/**
* Test whether the child rect fits completely inside the parent rect.
* @param {ClientRect} parent