DEV Community

Cover image for Keeping 'em Honest: Self-Validating Objects
Sean Travis Taylor
Sean Travis Taylor

Posted on

Keeping 'em Honest: Self-Validating Objects

What is a program? A program is a series of state changes that accomplishes something. It may be a game or a piece of CAD software. It may be a graphic design application or a script that automates data entry. Whatever the endeavor, a program's purpose is to do something and achieve that something over a series of changes in state.

A simple calculator offers an example. We begin with a value and through a series of operations (state changes) we produce a result: a sum, a product, a difference or a dividend.

What is a bug? A programmer's headache certainly but what is it really? It is a result of an invalid state change:

  • division by zero. A compiler will reject this computation
  • sending an HTTP POST request to create a new resource without authorization
  • subtracting a number from an array via the minus (-) operator

The list above presents invalid state changes within a programming language or within the business rules of an application. The programmer's mandate is to avoid such changes; when they occur, it is the programmer's job to ensure the program recovers.

Strong typing helps in this regard. So do guard clauses and validation packages like Joi or Ajv. If none of these are in place in our programs, we should begin with the above. Such approaches are certainly better than nothing yet fail to aid the program at its most vulnerable: during execution.

Guard clauses require sprinkling if/else statements throughout our codebases and they create new states our program can assume. They also require immense rigor. We discussed the dangers of a gauntlet of if/else statements in a previous post.

Type-safe languages or typing packages offer confidence but serve best as we write code. They prevent introduction of invalid state changes into the program in the first place. Yet whether provided by a programming language or some third-party package, strong typing offers less protection when our program runs.

At runtime we pray we've done our best to catch exceptions whenever they occur and at least log a descriptive message that leaves us some clue. The current state of the art encourages us to catch invalid state changes through vigilance and explicit checks. We can also just wait for the program to explode.

How can we design objects such that invalid state transitions instantly create exceptions?

How do we avoid passing invalid data around our application until a guard clause or validation exercise uncovers an invariant is violated?

Can we make invalid state transitions impossible?

We present the Self-validating Object: an object that ensures its own validity and notifies us when validation criteria are violated.

import Ajv from 'ajv';

/**
 * @param {Object} schema - a JSON Schema document to use for validation
 * @param {Object} options - valid JSON Schema configuration option see: https://ajv.js.org/options.html
 */
function Validator(schema, options = {}) {
  const ajv = new Ajv(options);

  /**
   * Runs validation of an incoming object
   * @param {Object} data - an object to be validated against the `schema`
   */
  function validate(data) {
    const valid = ajv.validate(schema, data);
    if (!valid) {
      throw new Error(
        `Validation error: ${ajv.errors
          .map((e) => JSON.stringify(e))
          .join(', ')}`
      );
    }
  }

  return {
    validate,
  };
}

/**
 * Factory function for Self-Validating Objects
 * @param {Object} schema - a JSON Schema object
 * @param {Object} initialData
 */
function SelfValidatingObject(schema, initialData = {}) {
  const validator = Validator(schema);
  let __data = { ...initialData };

  /**
   * @throws Will throw exception if data validation fails
   */
  function validate() {
    validator.validate(__data);
  }

  const proxy = new Proxy(
    {},
    {
      set: (target, prop, value) => {
        __data[prop] = value; // Set the new value
        validate(); // Validate new data state; may throw exception
        return true; // Indicate successful property set
      },
      get: (target, prop) => {
        if (prop === 'value') {
          return () => __data; // Provides access to the whole object
        }
        return __data[prop]; // Default get operation
      },
    }
  );

  validate(); // Initial validation of the provided data
  return proxy;
}

Enter fullscreen mode Exit fullscreen mode

Our implementation uses JSON Schema and Ajv as a validation handler but you can adjust this pattern with your own validation library as well.

The key to this pattern is the ES6 Proxy class. We won't explore this class in this post but you can learn more here and here.

The Proxy class intercepts changes on an object. Our implementation automatically validates the object whenever a field is set or updated. This validation is executed against the JSON Schema we provide on object creation.


const schema = {
  type: 'object',
  properties: {
    foo: { type: 'integer' },
    bar: { type: 'string', minLength: 5 },
  },
  required: ['foo', 'bar'],
  additionalProperties: false,
};

try {
  const fooObject = SelfValidatingObject(schema, { foo: 1, bar: '12345' });
  console.log('Object is valid:', fooObject.value());

// This throws an exception because '123' is not a valid string for 'bar'
  fooObject.bar = '1234';
} catch (ex) {
  console.error(ex.message);
}
Enter fullscreen mode Exit fullscreen mode

We learn the instant an object enters an invalid state and raise an exception. No stepping through code to learn when an object property becomes undefined. Our exception also includes the breached validation rules for our logs. This means no more logging objects to trace surprising state changes.

Since validation logic is encapsulated within our object, we know the validity of our object wherever it travels in our codebase.

When Self-validating Objects are combined with patterns explored in previous posts, we get a robust codebase with fewer bugs owing to unpredictable state changes.

Self-validating Objects render it impossible for objects to exist in invalid states. Whether it's the implementation shown above or something different, thinking deeply about how to prevent invalid state changes is a key activity during the design of any program.

Top comments (0)