Skip to content

Commit e409be5

Browse files
pushredepicfaace
authored andcommitted
Custom string formats (#1186)
* Accept custom string formats via `additionalFormats` prop Follow AJV extension pattern established for meta schemas and passthrough to AJV's addFormat method * Wire additionalFormats prop * additionalFormats → customFormats * Fix and test custom format prop updates * Update docs/validation.md Co-Authored-By: pushred <[email protected]>
1 parent f8d0d45 commit e409be5

File tree

5 files changed

+169
-8
lines changed

5 files changed

+169
-8
lines changed

docs/validation.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,30 @@ render((
4747
> received as second argument.
4848
> - The `validate()` function is called **after** the JSON schema validation.
4949
50+
### Custom string formats
51+
52+
[Pre-defined semantic formats](https://json-schema.org/latest/json-schema-validation.html#rfc.section.7) are limited. react-jsonschema-form adds two formats, `color` and `data-url`, to support certain [alternative widgets](form-customization.md#alternative-widgets). You can add formats of your own through the `customFormats` prop to your `Form` component:
53+
54+
```jsx
55+
const schema = {
56+
phoneNumber: {
57+
type: 'string',
58+
format: 'format-us'
59+
}
60+
};
61+
62+
const customFormats = {
63+
'phone-us': /\(?\d{3}\)?[\s-]?\d{3}[\s-]?\d{4}$/
64+
};
65+
66+
render((
67+
<Form schema={schema}
68+
customFormats={customFormats}/>
69+
), document.getElementById("app"));
70+
```
71+
72+
Format values can be anything AJV’s [`addFormat` method](https://github.com/epoberezkin/ajv#addformatstring-name-stringregexpfunctionobject-format---ajv) accepts.
73+
5074
### Custom schema validation
5175

5276
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:

src/components/Form.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,10 @@ 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+
const customFormats = props.customFormats;
6162
const additionalMetaSchemas = props.additionalMetaSchemas;
6263
const { errors, errorSchema } = mustValidate
63-
? this.validate(formData, schema, additionalMetaSchemas)
64+
? this.validate(formData, schema, additionalMetaSchemas, customFormats)
6465
: {
6566
errors: state.errors || [],
6667
errorSchema: state.errorSchema || {},
@@ -91,7 +92,8 @@ export default class Form extends Component {
9192
validate(
9293
formData,
9394
schema = this.props.schema,
94-
additionalMetaSchemas = this.props.additionalMetaSchemas
95+
additionalMetaSchemas = this.props.additionalMetaSchemas,
96+
customFormats = this.props.customFormats
9597
) {
9698
const { validate, transformErrors } = this.props;
9799
const { definitions } = this.getRegistry();
@@ -101,7 +103,8 @@ export default class Form extends Component {
101103
resolvedSchema,
102104
validate,
103105
transformErrors,
104-
additionalMetaSchemas
106+
additionalMetaSchemas,
107+
customFormats
105108
);
106109
}
107110

@@ -301,6 +304,7 @@ if (process.env.NODE_ENV !== "production") {
301304
transformErrors: PropTypes.func,
302305
safeRenderCompletion: PropTypes.bool,
303306
formContext: PropTypes.object,
307+
customFormats: PropTypes.object,
304308
additionalMetaSchemas: PropTypes.arrayOf(PropTypes.object),
305309
};
306310
}

src/validate.js

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Ajv from "ajv";
33
let ajv = createAjvInstance();
44
import { deepEquals } from "./utils";
55

6+
let formerCustomFormats = null;
67
let formerMetaSchema = null;
78

89
import { isObject, mergeObjects } from "./utils";
@@ -167,19 +168,35 @@ export default function validateFormData(
167168
schema,
168169
customValidate,
169170
transformErrors,
170-
additionalMetaSchemas = []
171+
additionalMetaSchemas = [],
172+
customFormats = {}
171173
) {
174+
const newMetaSchemas = !deepEquals(formerMetaSchema, additionalMetaSchemas);
175+
const newFormats = !deepEquals(formerCustomFormats, customFormats);
176+
177+
if (newMetaSchemas || newFormats) {
178+
ajv = createAjvInstance();
179+
}
180+
172181
// add more schemas to validate against
173182
if (
174183
additionalMetaSchemas &&
175-
!deepEquals(formerMetaSchema, additionalMetaSchemas) &&
184+
newMetaSchemas &&
176185
Array.isArray(additionalMetaSchemas)
177186
) {
178-
ajv = createAjvInstance();
179187
ajv.addMetaSchema(additionalMetaSchemas);
180188
formerMetaSchema = additionalMetaSchemas;
181189
}
182190

191+
// add more custom formats to validate against
192+
if (customFormats && newFormats && isObject(customFormats)) {
193+
Object.keys(customFormats).forEach(formatName => {
194+
ajv.addFormat(formatName, customFormats[formatName]);
195+
});
196+
197+
formerCustomFormats = customFormats;
198+
}
199+
183200
let validationError = null;
184201
try {
185202
ajv.validate(schema, formData);
@@ -198,7 +215,13 @@ export default function validateFormData(
198215
typeof validationError.message === "string" &&
199216
validationError.message.includes("no schema with key or ref ");
200217

201-
if (noProperMetaSchema) {
218+
const unknownFormat =
219+
validationError &&
220+
validationError.message &&
221+
typeof validationError.message === "string" &&
222+
validationError.message.includes("unknown format");
223+
224+
if (noProperMetaSchema || unknownFormat) {
202225
errors = [
203226
...errors,
204227
{
@@ -212,7 +235,7 @@ export default function validateFormData(
212235

213236
let errorSchema = toErrorSchema(errors);
214237

215-
if (noProperMetaSchema) {
238+
if (noProperMetaSchema || unknownFormat) {
216239
errorSchema = {
217240
...errorSchema,
218241
...{

test/Form_test.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1979,6 +1979,53 @@ describe("Form", () => {
19791979
});
19801980
});
19811981

1982+
describe("Custom format updates", () => {
1983+
it("Should update custom formats when customFormats is changed", () => {
1984+
const formProps = {
1985+
liveValidate: true,
1986+
formData: {
1987+
areaCode: 123,
1988+
},
1989+
schema: {
1990+
type: "object",
1991+
properties: {
1992+
areaCode: {
1993+
type: "number",
1994+
format: "area-code",
1995+
},
1996+
},
1997+
},
1998+
uiSchema: {
1999+
areaCode: {
2000+
"ui:widget": "area-code",
2001+
},
2002+
},
2003+
widgets: {
2004+
"area-code": () => <div id="custom" />,
2005+
},
2006+
};
2007+
2008+
const { comp } = createFormComponent(formProps);
2009+
2010+
expect(comp.state.errorSchema).eql({
2011+
$schema: {
2012+
__errors: [
2013+
'unknown format "area-code" is used in schema at path "#/properties/areaCode"',
2014+
],
2015+
},
2016+
});
2017+
2018+
setProps(comp, {
2019+
...formProps,
2020+
customFormats: {
2021+
"area-code": /\d{3}/,
2022+
},
2023+
});
2024+
2025+
expect(comp.state.errorSchema).eql({});
2026+
});
2027+
});
2028+
19822029
describe("Meta schema updates", () => {
19832030
it("Should update allowed meta schemas when additionalMetaSchemas is changed", () => {
19842031
const formProps = {

test/validate_test.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,69 @@ describe("Validation", () => {
163163
});
164164
});
165165

166+
describe("validating using custom string formats", () => {
167+
const schema = {
168+
type: "object",
169+
properties: {
170+
phone: {
171+
type: "string",
172+
format: "phone-us",
173+
},
174+
},
175+
};
176+
177+
it("should return a validation error if unknown string format is used", () => {
178+
const result = validateFormData({ phone: "800.555.2368" }, schema);
179+
const errMessage =
180+
'unknown format "phone-us" is used in schema at path "#/properties/phone"';
181+
182+
expect(result.errors[0].stack).include(errMessage);
183+
expect(result.errorSchema).to.eql({
184+
$schema: { __errors: [errMessage] },
185+
});
186+
});
187+
188+
it("should return a validation error about formData", () => {
189+
const result = validateFormData(
190+
{ phone: "800.555.2368" },
191+
schema,
192+
null,
193+
null,
194+
null,
195+
{ "phone-us": /\(?\d{3}\)?[\s-]?\d{3}[\s-]?\d{4}$/ }
196+
);
197+
198+
expect(result.errors).to.have.lengthOf(1);
199+
expect(result.errors[0].stack).to.equal(
200+
'.phone should match format "phone-us"'
201+
);
202+
});
203+
204+
it("prop updates with new custom formats are accepted", () => {
205+
const result = validateFormData(
206+
{ phone: "abc" },
207+
{
208+
type: "object",
209+
properties: {
210+
phone: {
211+
type: "string",
212+
format: "area-code",
213+
},
214+
},
215+
},
216+
null,
217+
null,
218+
null,
219+
{ "area-code": /\d{3}/ }
220+
);
221+
222+
expect(result.errors).to.have.lengthOf(1);
223+
expect(result.errors[0].stack).to.equal(
224+
'.phone should match format "area-code"'
225+
);
226+
});
227+
});
228+
166229
describe("Custom validate function", () => {
167230
let errors, errorSchema;
168231

0 commit comments

Comments
 (0)