Skip to content

First-class support for comprehensive runtime validation  #160

Open
@sisp

Description

@sisp

mobx-keystone is opinionated about structuring data in a tree. This strong assumption enables many useful features such as references, snapshots etc. for which mobx-keystone has first-class support and which makes it such a great library. In my opinion, one feature is missing though: runtime validation of models which collects all errors rather than throwing an exception at the first encountered error.

Validation of user input is a common task in web development. When users enter malformed or otherwise invalid data, it is important to present them with feedback in order to help them correct their mistakes. Libraries such as mobx-keystone and mobx-state-tree offer runtime type checking inspired by io-ts and its predecessors, which follow the principle of domain-driven design. mobx-keystone's runtime types are great as guards against invalid data, but an exception is thrown at the first encountered error which means they cannot be used to build a comprehensive feedback system. But I think there is only a small gap between the current runtime types and ones that can collect all errors.

To give an example: Let's say a model prop must be an integer in the range 0-10 in order to be valid. Right now, I could create a refinement of the integer type to validate the value range. This prop can be edited by a user using a form field, and let's assume the user enters a non-integer number or an integer outside this range. I wouldn't want the app to throw an exception. Instead, I'd like to get an error message that tells the user to enter an integer in the range 0-10. This could be achieved by enforcing a number-typed value (where an exception is thrown if a non-number value is set) while the semantic constraints (integer in the range 0-10) are validated gracefully.

If you (@xaviergonz, and of course also others) are interested in this feature, I'd like to discuss ideas how it could be implemented in mobx-keystone.

I currently see the following requirements to make this sufficiently generic in order to cover a variety of use cases:

  • Error messages should be customizable.
  • The data structure of an error should be customizable to support for example:
    • error codes
    • error levels
    • additional structured error information
    • ...
  • The relationship of errors (using a union type leads to disjunctive errors) should be retained in the error collection in order to be able to present users with correct feedback about alternative errors.
  • Error information should be aggregated towards the root of the tree, so that the root model contains the complete collection of errors. This is useful, e.g., to determine whether a state tree contains any errors at all and to display all errors in a state tree in a dedicated view (e.g. think of VS Code's error panel).
  • The path of the error in the state tree (relative to the model from where the error collection of the subtree is accessed) should be available.
  • It should also be possible to validate computed properties and volatile state of a model.

I think there are a couple of design decisions to be made:

  • Is it necessary to create a new set of runtime types that collect errors instead of throwing exceptions, or can the current ones be extended?
  • How would the collected errors be exposed? As a builtin computed model property (e.g. model.$errors similar to model.$ with regard to naming)?
  • How to make the errors customizable (as mentioned above)?
  • How to propagate errors from a parent model to its children models? Imagine a composition of models where a parent model adds additional constraints to (some of) its children. When a child model is used in the context of a parent model, the validation errors of the child model that are added by the parent model should also be included in the error collection of the child model.
  • ...?

What do you think? If you're interested in having this feature added to mobx-keystone, I already have some experience with the design of some of the parts that I'd be happy to contribute to the discussion.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions