Skip to content

Commit 92233e4

Browse files
aerfioepicfaace
authored andcommittedMar 3, 2019
Add more schemas to validate against (#1130)
* Add more schemas to validate against * Edit validate.js so that it calls addMetaSchema conditionally * Add props which accepts custom schemas for ajv * remove unnecesary fallback * rename variable name * add documentation for `metaSchema` prop * Update docs/validation.md Co-Authored-By: aerfio <mati6095@gmail.com> * Update src/validate.js Co-Authored-By: aerfio <mati6095@gmail.com> * Update src/validate.js Co-Authored-By: aerfio <mati6095@gmail.com> * Update docs/validation.md Co-Authored-By: aerfio <mati6095@gmail.com> * Update docs/validation.md Co-Authored-By: aerfio <mati6095@gmail.com> * Update docs/validation.md Co-Authored-By: aerfio <mati6095@gmail.com> * return errors from validation in validateFormData * add initial tests for meta schemas * add one proper test case * add more test cases * Update docs/validation.md Co-Authored-By: aerfio <mati6095@gmail.com> * Add test cases for multiple metaSchemas in validateFormData * Change prop name and update docs * Change additionalMetaSchemas prop to array only and remove console.error * make sure that validateFormData's additionalMetaShemas argument is always an array * Commit "broken" test to see whether they pass * Fix tests and create new Ajv instance when additionalMetaSchemas changes * fix: create new ajv instance when additionalMetaSchemas prop is null, add tests * handle validation errors * fix tests and delete validation errors * Delete unneeded field in return statement * dont stop showing red overlay when validation errors are present * Make errors and test more sensible * rename variable * handle only $schema related errors * fix tests
1 parent aebfab9 commit 92233e4

9 files changed

+615
-177
lines changed
 

‎docs/validation.md

+33
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,39 @@ render((
4747
> received as second argument.
4848
> - The `validate()` function is called **after** the JSON schema validation.
4949
50+
### Custom schema validation
51+
52+
To have your schemas validated against any other meta schema than draft-07 (the current version of [JSON Schema](http://json-schema.org/)), make sure your schema has a `$schema` attribute that enables the validator to use the correct meta schema. For example:
53+
54+
```json
55+
{
56+
"$schema": "http://json-schema.org/draft-04/schema#",
57+
...
58+
}
59+
```
60+
61+
Note that react-jsonschema-form only supports the latest version of JSON Schema, draft-07, by default. To support additional meta schemas pass them through the `additionalMetaSchemas` prop to your `Form` component:
62+
63+
```jsx
64+
const additionalMetaSchemas = require("ajv/lib/refs/json-schema-draft-04.json");
65+
66+
render((
67+
<Form schema={schema}
68+
additionalMetaSchemas={[additionalMetaSchemas]}/>
69+
), document.getElementById("app"));
70+
```
71+
72+
In this example `schema` passed as props to `Form` component can be validated against draft-07 (default) and by draft-04 (added), depending on the value of `$schema` attribute.
73+
74+
`additionalMetaSchemas` also accepts more than one meta schema:
75+
76+
```jsx
77+
render((
78+
<Form schema={schema}
79+
additionalMetaSchemas={[metaSchema1, metaSchema2]} />
80+
), document.getElementById("app"));
81+
```
82+
5083
### Custom error messages
5184

5285
Validation error messages are provided by the JSON Schema validation by default. If you need to change these messages or make any other modifications to the errors from the JSON Schema validation, you can define a transform function that receives the list of JSON Schema errors and returns a new list.

‎package-lock.json

+310-137
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"react": ">=15"
4444
},
4545
"dependencies": {
46-
"ajv": "^5.2.3",
46+
"ajv": "^6.7.0",
4747
"babel-runtime": "^6.26.0",
4848
"core-js": "^2.5.7",
4949
"lodash.topath": "^4.5.2",

‎src/components/Form.js

+11-4
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@ export default class Form extends Component {
5858
const { definitions } = schema;
5959
const formData = getDefaultFormState(schema, props.formData, definitions);
6060
const retrievedSchema = retrieveSchema(schema, definitions, formData);
61-
61+
const additionalMetaSchemas = props.additionalMetaSchemas;
6262
const { errors, errorSchema } = mustValidate
63-
? this.validate(formData, schema)
63+
? this.validate(formData, schema, additionalMetaSchemas)
6464
: {
6565
errors: state.errors || [],
6666
errorSchema: state.errorSchema || {},
@@ -80,22 +80,28 @@ export default class Form extends Component {
8080
edit,
8181
errors,
8282
errorSchema,
83+
additionalMetaSchemas,
8384
};
8485
}
8586

8687
shouldComponentUpdate(nextProps, nextState) {
8788
return shouldRender(this, nextProps, nextState);
8889
}
8990

90-
validate(formData, schema = this.props.schema) {
91+
validate(
92+
formData,
93+
schema = this.props.schema,
94+
additionalMetaSchemas = this.props.additionalMetaSchemas
95+
) {
9196
const { validate, transformErrors } = this.props;
9297
const { definitions } = this.getRegistry();
9398
const resolvedSchema = retrieveSchema(schema, definitions, formData);
9499
return validateFormData(
95100
formData,
96101
resolvedSchema,
97102
validate,
98-
transformErrors
103+
transformErrors,
104+
additionalMetaSchemas
99105
);
100106
}
101107

@@ -295,5 +301,6 @@ if (process.env.NODE_ENV !== "production") {
295301
transformErrors: PropTypes.func,
296302
safeRenderCompletion: PropTypes.bool,
297303
formContext: PropTypes.object,
304+
additionalMetaSchemas: PropTypes.arrayOf(PropTypes.object),
298305
};
299306
}

‎src/components/fields/SchemaField.js

+10-8
Original file line numberDiff line numberDiff line change
@@ -102,17 +102,19 @@ function ErrorList(props) {
102102
if (errors.length === 0) {
103103
return <div />;
104104
}
105+
105106
return (
106107
<div>
107-
<p />
108108
<ul className="error-detail bs-callout bs-callout-info">
109-
{errors.map((error, index) => {
110-
return (
111-
<li className="text-danger" key={index}>
112-
{error}
113-
</li>
114-
);
115-
})}
109+
{errors
110+
.filter(elem => !!elem)
111+
.map((error, index) => {
112+
return (
113+
<li className="text-danger" key={index}>
114+
{error}
115+
</li>
116+
);
117+
})}
116118
</ul>
117119
</div>
118120
);

‎src/validate.js

+76-21
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,32 @@
11
import toPath from "lodash.topath";
22
import Ajv from "ajv";
3-
const ajv = new Ajv({
4-
errorDataPath: "property",
5-
allErrors: true,
6-
multipleOfPrecision: 8,
7-
});
8-
// add custom formats
9-
ajv.addFormat(
10-
"data-url",
11-
/^data:([a-z]+\/[a-z0-9-+.]+)?;(?:name=(.*);)?base64,(.*)$/
12-
);
13-
ajv.addFormat(
14-
"color",
15-
/^(#?([0-9A-Fa-f]{3}){1,2}\b|aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow|(rgb\(\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*,\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*,\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*\))|(rgb\(\s*(\d?\d%|100%)+\s*,\s*(\d?\d%|100%)+\s*,\s*(\d?\d%|100%)+\s*\)))$/
16-
);
3+
let ajv = createAjvInstance();
4+
import { deepEquals } from "./utils";
5+
6+
let formerMetaSchema = null;
177

188
import { isObject, mergeObjects } from "./utils";
199

10+
function createAjvInstance() {
11+
const ajv = new Ajv({
12+
errorDataPath: "property",
13+
allErrors: true,
14+
multipleOfPrecision: 8,
15+
schemaId: "auto",
16+
});
17+
18+
// add custom formats
19+
ajv.addFormat(
20+
"data-url",
21+
/^data:([a-z]+\/[a-z0-9-+.]+)?;(?:name=(.*);)?base64,(.*)$/
22+
);
23+
ajv.addFormat(
24+
"color",
25+
/^(#?([0-9A-Fa-f]{3}){1,2}\b|aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow|(rgb\(\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*,\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*,\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*\))|(rgb\(\s*(\d?\d%|100%)+\s*,\s*(\d?\d%|100%)+\s*,\s*(\d?\d%|100%)+\s*\)))$/
26+
);
27+
return ajv;
28+
}
29+
2030
function toErrorSchema(errors) {
2131
// Transforms a ajv validation errors list:
2232
// [
@@ -53,13 +63,16 @@ function toErrorSchema(errors) {
5363
}
5464
parent = parent[segment];
5565
}
66+
5667
if (Array.isArray(parent.__errors)) {
5768
// We store the list of errors for this node in a property named __errors
5869
// to avoid name collision with a possible sub schema field named
5970
// "errors" (see `validate.createErrorHandler`).
6071
parent.__errors = parent.__errors.concat(message);
6172
} else {
62-
parent.__errors = [message];
73+
if (message) {
74+
parent.__errors = [message];
75+
}
6376
}
6477
return errorSchema;
6578
}, {});
@@ -152,23 +165,62 @@ export default function validateFormData(
152165
formData,
153166
schema,
154167
customValidate,
155-
transformErrors
168+
transformErrors,
169+
additionalMetaSchemas = []
156170
) {
171+
// add more schemas to validate against
172+
if (
173+
additionalMetaSchemas &&
174+
!deepEquals(formerMetaSchema, additionalMetaSchemas) &&
175+
Array.isArray(additionalMetaSchemas)
176+
) {
177+
ajv = createAjvInstance();
178+
ajv.addMetaSchema(additionalMetaSchemas);
179+
formerMetaSchema = additionalMetaSchemas;
180+
}
181+
182+
let validationError = null;
157183
try {
158184
ajv.validate(schema, formData);
159-
} catch (e) {
160-
// swallow errors thrown in ajv due to invalid schemas, these
161-
// still get displayed
185+
} catch (err) {
186+
validationError = err;
162187
}
163188

164189
let errors = transformAjvErrors(ajv.errors);
165190
// Clear errors to prevent persistent errors, see #1104
191+
166192
ajv.errors = null;
167193

194+
const noProperMetaSchema =
195+
validationError &&
196+
validationError.message &&
197+
typeof validationError.message === "string" &&
198+
validationError.message.includes("no schema with key or ref ");
199+
200+
if (noProperMetaSchema) {
201+
errors = [
202+
...errors,
203+
{
204+
stack: validationError.message,
205+
},
206+
];
207+
}
168208
if (typeof transformErrors === "function") {
169209
errors = transformErrors(errors);
170210
}
171-
const errorSchema = toErrorSchema(errors);
211+
212+
let errorSchema = toErrorSchema(errors);
213+
214+
if (noProperMetaSchema) {
215+
errorSchema = {
216+
...errorSchema,
217+
...{
218+
$schema: {
219+
__errors: [validationError.message],
220+
},
221+
},
222+
};
223+
}
172224

173225
if (typeof customValidate !== "function") {
174226
return { errors, errorSchema };
@@ -182,7 +234,10 @@ export default function validateFormData(
182234
// properties.
183235
const newErrors = toErrorList(newErrorSchema);
184236

185-
return { errors: newErrors, errorSchema: newErrorSchema };
237+
return {
238+
errors: newErrors,
239+
errorSchema: newErrorSchema,
240+
};
186241
}
187242

188243
/**

‎test/ArrayField_test.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ describe("ArrayField", () => {
132132
const matches = node.querySelectorAll("#custom");
133133
expect(matches).to.have.length.of(1);
134134
expect(matches[0].textContent).to.eql(
135-
"should NOT have less than 2 items"
135+
"should NOT have fewer than 2 items"
136136
);
137137
});
138138

@@ -759,7 +759,7 @@ describe("ArrayField", () => {
759759
const matches = node.querySelectorAll("#custom");
760760
expect(matches).to.have.length.of(1);
761761
expect(matches[0].textContent).to.eql(
762-
"should NOT have duplicate items (items ## 0 and 1 are identical)"
762+
"should NOT have duplicate items (items ## 1 and 0 are identical)"
763763
);
764764
});
765765
});
@@ -860,7 +860,7 @@ describe("ArrayField", () => {
860860
const matches = node.querySelectorAll("#custom");
861861
expect(matches).to.have.length.of(1);
862862
expect(matches[0].textContent).to.eql(
863-
"should NOT have less than 3 items"
863+
"should NOT have fewer than 3 items"
864864
);
865865
});
866866
});
@@ -967,7 +967,7 @@ describe("ArrayField", () => {
967967
const matches = node.querySelectorAll("#custom");
968968
expect(matches).to.have.length.of(1);
969969
expect(matches[0].textContent).to.eql(
970-
"should NOT have less than 5 items"
970+
"should NOT have fewer than 5 items"
971971
);
972972
});
973973
});
@@ -1040,10 +1040,10 @@ describe("ArrayField", () => {
10401040
const matches = node.querySelectorAll("#custom-error");
10411041
expect(matches).to.have.length.of(2);
10421042
expect(matches[0].textContent).to.eql(
1043-
"should NOT have less than 3 items"
1043+
"should NOT have fewer than 3 items"
10441044
);
10451045
expect(matches[1].textContent).to.eql(
1046-
"should NOT have less than 2 items"
1046+
"should NOT have fewer than 2 items"
10471047
);
10481048
});
10491049
});

‎test/Form_test.js

+51
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
createComponent,
1010
createFormComponent,
1111
createSandbox,
12+
setProps,
1213
} from "./test_utils";
1314

1415
describe("Form", () => {
@@ -1977,4 +1978,54 @@ describe("Form", () => {
19771978
expect(node.getAttribute("novalidate")).not.to.be.null;
19781979
});
19791980
});
1981+
1982+
describe("Meta schema updates", () => {
1983+
it("Should update allowed meta schemas when additionalMetaSchemas is changed", () => {
1984+
const formProps = {
1985+
liveValidate: true,
1986+
schema: {
1987+
$schema: "http://json-schema.org/draft-04/schema#",
1988+
type: "string",
1989+
minLength: 8,
1990+
pattern: "d+",
1991+
},
1992+
formData: "short",
1993+
additionalMetaSchemas: [],
1994+
};
1995+
1996+
const { comp } = createFormComponent(formProps);
1997+
1998+
expect(comp.state.errorSchema).eql({
1999+
$schema: {
2000+
__errors: [
2001+
'no schema with key or ref "http://json-schema.org/draft-04/schema#"',
2002+
],
2003+
},
2004+
});
2005+
2006+
setProps(comp, {
2007+
...formProps,
2008+
additionalMetaSchemas: [
2009+
require("ajv/lib/refs/json-schema-draft-04.json"),
2010+
],
2011+
});
2012+
2013+
expect(comp.state.errorSchema).eql({
2014+
__errors: [
2015+
"should NOT be shorter than 8 characters",
2016+
'should match pattern "d+"',
2017+
],
2018+
});
2019+
2020+
setProps(comp, formProps);
2021+
2022+
expect(comp.state.errorSchema).eql({
2023+
$schema: {
2024+
__errors: [
2025+
'no schema with key or ref "http://json-schema.org/draft-04/schema#"',
2026+
],
2027+
},
2028+
});
2029+
});
2030+
});
19802031
});

‎test/validate_test.js

+117
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,71 @@ describe("Validation", () => {
9898
});
9999
});
100100

101+
describe("validating using custom meta schema", () => {
102+
const schema = {
103+
$ref: "#/definitions/Dataset",
104+
$schema: "http://json-schema.org/draft-04/schema#",
105+
definitions: {
106+
Dataset: {
107+
properties: {
108+
datasetId: {
109+
pattern: "\\d+",
110+
type: "string",
111+
},
112+
},
113+
required: ["datasetId"],
114+
type: "object",
115+
},
116+
},
117+
};
118+
const metaSchemaDraft4 = require("ajv/lib/refs/json-schema-draft-04.json");
119+
const metaSchemaDraft6 = require("ajv/lib/refs/json-schema-draft-06.json");
120+
121+
it("should return a validation error about meta schema when meta schema is not defined", () => {
122+
const errors = validateFormData(
123+
{ datasetId: "some kind of text" },
124+
schema
125+
);
126+
const errMessage =
127+
'no schema with key or ref "http://json-schema.org/draft-04/schema#"';
128+
expect(errors.errors[0].stack).to.equal(errMessage);
129+
expect(errors.errors).to.eql([
130+
{
131+
stack: errMessage,
132+
},
133+
]);
134+
expect(errors.errorSchema).to.eql({
135+
$schema: { __errors: [errMessage] },
136+
});
137+
});
138+
it("should return a validation error about formData", () => {
139+
const errors = validateFormData(
140+
{ datasetId: "some kind of text" },
141+
schema,
142+
null,
143+
null,
144+
[metaSchemaDraft4]
145+
);
146+
expect(errors.errors).to.have.lengthOf(1);
147+
expect(errors.errors[0].stack).to.equal(
148+
'.datasetId should match pattern "\\d+"'
149+
);
150+
});
151+
it("should return a validation error about formData, when used with multiple meta schemas", () => {
152+
const errors = validateFormData(
153+
{ datasetId: "some kind of text" },
154+
schema,
155+
null,
156+
null,
157+
[metaSchemaDraft4, metaSchemaDraft6]
158+
);
159+
expect(errors.errors).to.have.lengthOf(1);
160+
expect(errors.errors[0].stack).to.equal(
161+
'.datasetId should match pattern "\\d+"'
162+
);
163+
});
164+
});
165+
101166
describe("Custom validate function", () => {
102167
let errors, errorSchema;
103168

@@ -675,5 +740,57 @@ describe("Validation", () => {
675740
expect(node.querySelectorAll(".foo")).to.have.length.of(1);
676741
});
677742
});
743+
describe("Custom meta schema", () => {
744+
let onSubmit;
745+
let onError;
746+
let comp, node;
747+
const formData = {
748+
datasetId: "no err",
749+
};
750+
751+
const schema = {
752+
$ref: "#/definitions/Dataset",
753+
$schema: "http://json-schema.org/draft-04/schema#",
754+
definitions: {
755+
Dataset: {
756+
properties: {
757+
datasetId: {
758+
pattern: "\\d+",
759+
type: "string",
760+
},
761+
},
762+
required: ["datasetId"],
763+
type: "object",
764+
},
765+
},
766+
};
767+
768+
beforeEach(() => {
769+
onSubmit = sandbox.spy();
770+
onError = sandbox.spy();
771+
const withMetaSchema = createFormComponent({
772+
schema,
773+
formData,
774+
liveValidate: true,
775+
additionalMetaSchemas: [
776+
require("ajv/lib/refs/json-schema-draft-04.json"),
777+
],
778+
onSubmit,
779+
onError,
780+
});
781+
comp = withMetaSchema.comp;
782+
node = withMetaSchema.node;
783+
});
784+
it("should be used to validate schema", () => {
785+
expect(node.querySelectorAll(".errors li")).to.have.length.of(1);
786+
expect(comp.state.errors).to.have.lengthOf(1);
787+
expect(comp.state.errors[0].message).eql(`should match pattern "\\d+"`);
788+
Simulate.change(node.querySelector("input"), {
789+
target: { value: "1234" },
790+
});
791+
expect(node.querySelectorAll(".errors li")).to.have.length.of(0);
792+
expect(comp.state.errors).to.have.lengthOf(0);
793+
});
794+
});
678795
});
679796
});

0 commit comments

Comments
 (0)
Please sign in to comment.