Embracing Modern Data Validation in EmberJS
September 23, 2023
16 min read
In an EmberJS codebase I've been working with, Computed Properties and Mixins were previously used to handle Model validations, via
yup, and Tracked properties in order to leave behind these old patterns and provide stricter type safety.
The Why: So Long, Computed Properties
The previous paradigm worked as follows:
buildValidations is called, provided with an object of properties on the Model to be validated, with their values being the result of the
validator function - given a string representing the validator's name, and provided options, it returns a Computed Property. The returned result from
buildValidations is an EmberObject Mixin, which can then be extended by the Model class to be validated:
Why was this paradigm in need of changing? With recent releases, Ember has been moving away from Computed Properties and several other Ember Classic patterns, including Mixins - this left us in an interesting spot, where Computed Properties and Mixins aren't technically deprecated, but it's safe to say they will be soon, and usage of either is discouraged. And that notwithstanding, they don't function quite right with Typescript - They work, but are difficult to use in otherwise-strictly-typed contexts and have issues when using Tracked primitives as dependant keys.
So, how do Tracked properties come into this? As a modern replacement for Computed Properties, they simplify state management and address some flaws of Computed Properties. By annotating a property with the
@tracked decorator, Ember is informed that this particular field is likely to change over time and should trigger any dependent computations when it does. The result is a more straightforward, more explicit, less boilerplate-heavy approach to reactive state management.
@cached decorator from
@glimmer/tracking can be used to cache the getter's value and avoid unnecessary recomputation.
All that to say - there's many benefits from this switch ✨
Choosing a Library
The Landscape of Validation Libraries
- zod: Known for robustness and its rich feature set,
zodexcels in static type inference and allows for the use of Union and Intersection types.
- io-ts: Originating from the functional programming world,
io-tsoffers an extremely type-safe validation mechanism. The library is based in functional programming paradigms.
yupwas later refactored to TypeScript. It offers good ease-of-use and relatively strong TypeScript support, albeit with some limitations in type inference and missing schema functionality.
Some other libraries focusing more on input validation are:
- validator.js: Great for simple, string-based validations, i.e. for form inputs, but it lacks the rich feature set and type safety required for more complex data structures.
- Joi: Initially built for Hapi.js but usable standalone,
Joiis another popular validation library. It's highly extensible but, like
validator.js, less focused on type safety and more verbose compared to
- class-validator: Works with TypeScript classes using decorators for validation logic. While tightly integrated with TypeScript, it couples validation logic directly with data models, which wasn't ideal for me.
There's also an entirely different class of libraries, like
react-hook-form which are framework-specific and concentrate on form validation. These can be useful for front-end validation for their respective UI library, but they don't offer the same flexibility as
zod when it comes to validating arbitrary data structures, nor do they usually offer type safety to the same degree.
zod), but it's more than sufficient for most use-cases. Third,
yup provides a high degree of flexibility, allowing for the validation of various data structures, not just form inputs or DOM elements.
yup stood out as a balanced choice for my use-case. It allows for easy schema definitions and validations, offers a fair level of type inference, and most importantly, doesn't require adopting a radically different programming paradigm. It is worth acknowledging its main shortcoming: schema type inference inaccuracies. Its type inference indicates that object properties are mandatory, when in actuality they're optional. In my opinion? The overall simplicity and developer experience offered outweighs this limitation. If you want truly strict & precise types, then
io-ts may be more compelling options.
Implementing the Utility
My objective was to offer type-safe validations for Models (or any other objects/classes, really), similar enough in structure to what
ember-cp-validations provided in order to reduce refactoring work. Besides switching out validation schema and Mixins in Models, I wanted it to be (more or less) a "drop-in" replacement.
YupValidations class takes in two type parameters,
T extends FrameworkObject, representing the context where validation happens (often, this will be an EmberData Model), and
S extends ObjectShape, representing a base
yup schema of an empty object. On instantiation, the class takes the calling context and the schema shape to be used, initializing the schema with
Tracking Validation State
@tracked decorator is used in the class to track any current validation errors.
tracked-built-ins) is needed for the validation state, since Glimmer's
@tracked decorator doesn't deep-track objects. This ensures any components accessing validations re-render when the validations change or are re-run.
Typed Validation Results
The class employs some utility types to type validation results based on the schema shape.
FieldErrors<T> creates a mapping from each schema field to its validation messages, validity, and current/previous value.
SchemaType<S, T> represents the shape of the given schema, and
ValidationResult<S, T> represents the returned validation result.
validate method triggers validations. It validates all context properties and updates the tracked
fieldErrors is a getter which simply presents any existing
ValidationErrors in a more manageable shape. Likewise for
Putting it All Together: Usage Example
Now that we've looked at the internals of the utility, here's an example of it in action - suppose you have a Route where you're dealing with an instance of
MyModel and want to validate it before persisting any changes.
First, the model in question:
Now, let's call
validate() on its Validator instance:
Building this out, I had a chance to dive much deeper into TypeScript utility types and generics than previously. I'm quite happy with the end result; a type-safe validation utility which can be used across our Ember application with confidence. Swapping out Computed Properties for Tracked properties isn't just a box-ticking exercise; it's a necessary step towards making the codebase more maintainable and future-proof.
My choice to use
yup over other validation libraries came after weighing the trade-offs for this specific use-case. Ultimately, I think any of the mentioned libraries would be great fits, it just came down to my preference for
I hope sharing this experience offers some valuable insights or ideas for your own projects, whether you're working with Ember or any other framework. If you have questions or want to discuss any part of this in more detail (cough cough, or if I made any mistakes), !