Description
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 tomodel.$
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.