React-jsonschema-form: Discuss - Asynchronous validation

Created on 4 Aug 2019  路  11Comments  路  Source: rjsf-team/react-jsonschema-form

There have been many attempts to implement async validation in the past, and it seems like it merits a discussion on what the best approach is to implement this. Making this issue as a wrapper issue for async validation in general.

Possible solutions that have been proposed / tried so far:

  • Allow validate() to return a promise (#197, #198 - @n1k0 , #401 - @Kyr , #757 -@olzraiti )
  • Allow errors to be passed into a form as a prop (#155, #874 - @davidbarratt, #1383 - @thijssteel)
  • Use ajv's asynchronous validation functionality (#1210)
  • Create a separate onAsyncValidate prop (#855 - @laithalissa)

It would be good to discuss the benefits and drawbacks of these approaches (such as: ease of use, backwards compatibility, etc.), and see what would be the best option to do going forward. I've tagged the people who've tried to implement async validation before on this issue, but would appreciate any feedback or thoughts!

Most helpful comment

@epicfaace In general I like the idea of async validation. Note there is also a race condition between validation and submission: e.g. a sign up form could validate a username for availability, but between successful validation and submission the username can be taken by someone else. Submission errors and validation errors are separate kinds in that respect.

When interacting with APIs, submission errors are almost always available. I don't know of many APIs providing validation endpoints.

As a separate thought: perhaps for ease of use, validation can be made to be more granular: e.g. declaring validation callbacks for individual fields/objects.

All 11 comments

I just ran into this limitation today. My initial thought is to support validate() returning a promise. However, thinking about more use-cases makes me lean towards an errors prop. This is a more declarative approach and would enable many other scenarios. I still think there is value in exposing the errors generated by AJV, but via a callback (onValidate(errors)). This could be added incrementally as well essentially allowing errors to be controlled (errors prop + onValidate(errors)) or uncontrolled (validate()).

Another point to discuss is the specific format of the errors prop (if we decide to go that route).
We could use the ajv format, which is more standardised and is likely to integrate better with the validation result by some backend.
We could also use the more 'objecty' format used internally.

Something that definitely should be considered as well is whether the async validation should block form submission.

I think a very common use case for async validation is displaying errors from a failed request to some backend and hence should not block submission.
However, another example could be validation of some hash or format that calls an async library client side and this might well require blocking.

I am currently researching if we can use this project as a replacement for something home grown and I have the following issue that would probably be solved by adding a prop:

We have extra validation on the backend which is returned as JSON with 400 http status on the submit request. I have not yet found a way to show these errors in the form. Adding a prop would probably solve my problem.

I'm curious as to why using a prop is better and more expansive than an asynchronous validate() function. What are some examples of use cases that would be difficult/impossible using an asynchronous validate() function, but greatly simplified using an errors prop?

@epicfaace one advantage is that it allows the caller to fully control the validation cycle. Giving control to the library to invoke a validate() function limits my ability to control when that gets invoked.

For instance, if I have an expensive operation to validate (i.e a network request), I may only want to do that under certain conditions.

@thijssteel @beskhue continuing the conversation from #1444 on this thread, as it seems more appropriate

Here's the concern I have about using an extraErrors prop as done in #1444: for the common scenario in which a server-side request is sent to do validation, it takes a lot of code to get this right. A good solution must:

  • In onSubmit, call the server-side request, get the errors, then set the extraErrors prop if there are errors; otherwise, submit.
  • Make sure that someone cannot call onSubmit once again before the previous server-side request, to avoid a race case.

Here's how that might look in practice:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      validating: false,
      extraErrors: null
    }
  }
  async onSubmit(e) {
    if (this.state.validating) {
      return;
    }

    this.setState({validating: true});
    const response = await fetch("/validate");
    if (response.body.success) {
      // Now we've submitted the form.
      console.log("submitted form");
    }
    else {
      this.setState({
        validating: false,
        extraErrors: response.body.extraErrors,
      });
    }
  }
  render() {
    return <Form
      onSubmit={this.onSubmit}
      extraErrors={this.props.extraErrors}
    />
  }
}

Note that because of these constraints, we have to use a class-based component. Additionally, onSubmit is not just called on submission; it's essentially used to perform async validation as well.

Whereas, if we just allow for an async validate function, it seems more intuitive -- onSubmit is called only on form submission, and validate is used for form validation:

const onSubmit = () => {
  console.log("submitted form");
}
const validate = async (formData, errors) => {
  const response = await fetch("/validate");
  if (!response.body.success) {
    errors = {...errors, response.extraErrors};
  }
  return errors;
}
const App = () => <Form
  onSubmit={onSubmit}
  validate={validate}
/>;

@a-b-r-o-w-n

@epicfaace one advantage is that it allows the caller to fully control the validation cycle. Giving control to the library to invoke a validate() function limits my ability to control when that gets invoked.

For instance, if I have an expensive operation to validate (i.e a network request), I may only want to do that under certain conditions.

I see your point here -- I think it makes sense to have an errors and onValidate(errors) prop. It would allow for full flexibility, although perhaps for the common use case of async validation it might be simpler for the user to implement it using the async validate function.

@epicfaace i don't know anyone that uses a separate validation endpoint before sending a request. So calling validation multiple times would give you the same problem of multiple submissions.

And any large project i have worked on has some shared logic for loading and error handling, this would not all be required by the component

@epicfaace In general I like the idea of async validation. Note there is also a race condition between validation and submission: e.g. a sign up form could validate a username for availability, but between successful validation and submission the username can be taken by someone else. Submission errors and validation errors are separate kinds in that respect.

When interacting with APIs, submission errors are almost always available. I don't know of many APIs providing validation endpoints.

As a separate thought: perhaps for ease of use, validation can be made to be more granular: e.g. declaring validation callbacks for individual fields/objects.

@thijssteel @Beskhue good points! Let's go ahead with this and we can clarify in the documentation / future updates if the need arises.

Still need you guys to treat ajv.validate as an async function because of async keywords.

We have a requirement to display all options in a dropdown, but mark certain choices as invalid when they are present in the response of an api call.

we have this:

{
  $async: true,
  type: 'string',
  notInCollection: {
    source: '/api/v1/restricted-values',
    params: {}
  }
}

example from ajv themselves:

var ajv = new Ajv;
// require('ajv-async')(ajv);

ajv.addKeyword('idExists', {
  async: true,
  type: 'number',
  validate: checkIdExists
});


function checkIdExists(schema, data) {
  return knex(schema.table)
  .select('id')
  .where('id', data)
  .then(function (rows) {
    return !!rows.length; // true if record is found
  });
}

var schema = {
  "$async": true,
  "properties": {
    "userId": {
      "type": "integer",
      "idExists": { "table": "users" }
    },
    "postId": {
      "type": "integer",
      "idExists": { "table": "posts" }
    }
  }
};

var validate = ajv.compile(schema);

validate({ userId: 1, postId: 19 })
.then(function (data) {
  console.log('Data is valid', data); // { userId: 1, postId: 19 }
})
.catch(function (err) {
  if (!(err instanceof Ajv.ValidationError)) throw err;
  // data is invalid
  console.log('Validation errors:', err.errors);
});

But we can't do any of this because you treat ajv.validate as a synchronous call.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

marinav picture marinav  路  3Comments

Eric24 picture Eric24  路  3Comments

n1k0 picture n1k0  路  3Comments

norim13 picture norim13  路  3Comments

elyobo picture elyobo  路  3Comments