Repository: mozilla-services/react-jsonschema-form
Pull request #432 (2016-12-22 21:35:36)

Add transform hook for changing errors from json schema validation
### Reasons for making this change

The error messages that come from the json schema spec/library (not sure where they come from) aren't very user friendly. They also clash with a pattern that the forms I work have for required validation, namely that those required errors should show up at the field level (rather than at the containing object).

One way of addressing those issues is to allow users to modify the error list sent back by the jsonschema library. Since that has all the error detail produced beyond just a message, it's easy to modify or write your own messages. This PR adds that hook.

I'm skipping updating tests and docs until I get some feedback on whether this is a good approach and something that this library should accommodate. 

Thanks!

### Checklist

* [x] **I'm updating documentation**
  - [x] I've checked the rendering of the Markdown text I've added
  - [x] If I'm adding a new section, I've updated the Table of Content
* [x] **I'm adding or updating code**
  - [x] I've added and/or updated tests
  - [x] I've updated docs if needed
* [x] **I'm adding a new feature**
  - [x] I've updated the playground with an example use of the feature


n1k0: This is actually smart, I like it. Sure I'd love if the jsonschema lib we used exposed a better API, but I think this nicely solves our current needs with this issue.

Could you please rebase your patch against latest master, and add a few tests and docs? Sorry for being so late to provide feedback.

Patch:
diff --git a/README.md b/README.md
index bb9e53526..29b13c593 100644
--- a/README.md
+++ b/README.md
@@ -66,6 +66,7 @@ A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) i
   - [Form data validation](#form-data-validation)
      - [Live validation](#live-validation)
      - [Custom validation](#custom-validation)
+     - [Custom error messages](#custom-error-messages)
      - [Error List Display](#error-list-display)
   - [Styling your forms](#styling-your-forms)
   - [Schema definitions and references](#schema-definitions-and-references)
@@ -1170,6 +1171,36 @@ render((
 >   received as second argument.
 > - The `validate()` function is called **after** the JSON schema validation.
 
+### Custom error messages
+
+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. 
+
+```js
+function transformErrors(errors) {
+  return errors.map(error => {
+    if (error.name === "pattern") {
+      error.message = "Only digits are allowed"
+    }
+    return error;
+  });
+}
+
+const schema = {
+  type: "object",
+  properties: {
+    onlyNumbersString: {type: "string", pattern: "\d*"},
+  }
+};
+
+render((
+  <Form schema={schema}
+        transformErrors={transformErrors} />
+), document.getElementById("app"));
+```
+
+> Notes:
+> - The `transformErrors()` function must return the list of errors. Modifying the list in place without returning it will result in an error.
+
 ### Error List Display
 
 To disable rendering of the error list at the top of the form, you can set the `showErrorList` prop to `false`. Doing so will still validate the form, but only the inline display will show.
diff --git a/playground/app.js b/playground/app.js
index 649ebd6b7..445e678f1 100644
--- a/playground/app.js
+++ b/playground/app.js
@@ -317,7 +317,8 @@ class App extends Component {
       validate,
       theme,
       editor,
-      ArrayFieldTemplate
+      ArrayFieldTemplate,
+      transformErrors
     } = this.state;
 
     return (
@@ -363,6 +364,7 @@ class App extends Component {
               onChange={this.onFormDataChange}
               fields={{geo: GeoPosition}}
               validate={validate}
+              transformErrors={transformErrors}
               onError={log("errors")} />}
         </div>
       </div>
diff --git a/playground/samples/validation.js b/playground/samples/validation.js
index 90739ab60..22452dca9 100644
--- a/playground/samples/validation.js
+++ b/playground/samples/validation.js
@@ -5,6 +5,17 @@ function validate({pass1, pass2}, errors) {
   return errors;
 }
 
+function transformErrors(errors) {
+  return errors.map(error => {
+    if (error.name === "minimum" && error.property === "instance.age") {
+      return Object.assign({}, error, {
+        message: "You need to be 18 because of some legal thing"
+      });
+    }
+    return error;
+  });
+}
+
 module.exports = {
   schema:  {
     title: "Custom validation",
@@ -21,6 +32,11 @@ module.exports = {
         type: "string",
         minLength: 3
       },
+      age: {
+        title: "Age",
+        type: "number",
+        minimum: 18
+      }
     }
   },
   uiSchema: {
@@ -28,5 +44,6 @@ module.exports = {
     pass2: {"ui:widget": "password"},
   },
   formData: {},
-  validate
+  validate,
+  transformErrors
 };
diff --git a/src/components/Form.js b/src/components/Form.js
index 89b83cd4f..de24535aa 100644
--- a/src/components/Form.js
+++ b/src/components/Form.js
@@ -60,8 +60,8 @@ export default class Form extends Component {
   }
 
   validate(formData, schema) {
-    const {validate} = this.props;
-    return validateFormData(formData, schema || this.props.schema, validate);
+    const {validate, transformErrors} = this.props;
+    return validateFormData(formData, schema || this.props.schema, validate, transformErrors);
   }
 
   renderErrors() {
@@ -202,6 +202,8 @@ if (process.env.NODE_ENV !== "production") {
     acceptcharset: PropTypes.string,
     noValidate: PropTypes.bool,
     liveValidate: PropTypes.bool,
+    validate: PropTypes.func,
+    transformErrors: PropTypes.func,
     safeRenderCompletion: PropTypes.bool,
     formContext: PropTypes.object,
   };
diff --git a/src/validate.js b/src/validate.js
index 371546160..0a7fe27a9 100644
--- a/src/validate.js
+++ b/src/validate.js
@@ -96,8 +96,11 @@ function unwrapErrorHandler(errorHandler) {
  * function, which receives the form data and an `errorHandler` object that
  * will be used to add custom validation errors for each field.
  */
-export default function validateFormData(formData, schema, customValidate) {
-  const {errors} = jsonValidate(formData, schema);
+export default function validateFormData(formData, schema, customValidate, transformErrors) {
+  let {errors} = jsonValidate(formData, schema);
+  if (typeof transformErrors === "function") {
+    errors = transformErrors(errors);
+  }
   const errorSchema = toErrorSchema(errors);
 
   if (typeof customValidate !== "function") {


Test patch:
diff --git a/test/validate_test.js b/test/validate_test.js
index fff8f796e..f3d12a028 100644
--- a/test/validate_test.js
+++ b/test/validate_test.js
@@ -94,6 +94,32 @@ describe("Validation", () => {
         ]);
       });
     });
+
+    describe("transformErrors", () => {
+      const illFormedKey = "bar.'\"[]()=+*&^%$#@!";
+      const schema = {
+        type: "object",
+        properties: {foo: {type: "string"}, [illFormedKey]: {type: "string"}}
+      };
+      const newErrorMessage = "Better error message";
+      const transformErrors = (errors) => {
+        return [
+          Object.assign({}, errors[0], {message: newErrorMessage})
+        ];
+      };
+
+      let errors;
+
+      beforeEach(() => {
+        const result = validateFormData({foo: 42, [illFormedKey]: 41}, schema, undefined, transformErrors);
+        errors = result.errors;
+      });
+
+      it("should use transformErrors function", () => {
+        expect(errors).not.to.be.empty;
+        expect(errors[0].message).to.equal(newErrorMessage);
+      });
+    });
   });
 
   describe("Form integration", () => {