Multi-Step Validation

Creating validators that reference data from earlier validation steps

Say we're receiving data that is supposed to conform to the following TypeScript interface:

interface CsvData {
  metadata: {
    keys: string[]
  }
  entries: object[]
}

We'd like to create a validator instance that verifies that the data we're receiving actually conforms to the above interface. In addition to this, we'd also like to verify that the strings found in <csvData>.metadata.keys are keys on each object found in <csvData>.entries. How would we do this? How do we configure part of a validator to change its behavior, based on data that was found earlier in the validation process?

Turns out, .lazy() gives us just what we need to accomplish this.

const andExpectKeysToBePresent = keys => validator.expectTo(object => {
  const invalidKey = keys.find(key => !(key in object));
  return invalidKey === undefined ? undefined : `have the key ${invalidKey}.`;
});

const csvDataValidator = validator`
  {
    metadata: {
      keys: string[]
    }
  } & ${validator.lazy(csvData => {
    const keys = csvData.metadata.keys;
    return validator`{
      entries: (object & ${andExpectKeysToBePresent(keys)})[]
    }`;
  })}
`;

So what's going on here?

We begin by using & to break up the validation into two steps. First, everything to the left of the & must be validated. Only after the left-hand side is validated will the right-hand side start executing. The callback provided to validator.lazy() will receive, as a parameter, the data that it is in charge of validating. Because validator.lazy() is interpolated after we've validated the metadata property, we know with certainty that the csvData received by the callback has, at a minimum, a metadata property that's well-formed. Inside the .lazy() callback, we can extract csvData.metadata.keys and use it to configure how the next half will be validated.

Any time data from earlier in the validation process needs to be used to configure how the validator behaves later on, this general pattern can be used.

validator`
  ...validate first part... &
  ${validator.lazy(data => {
    ...extract information from data...
    return validator`...validate second part using extracted information...`;
  })}
`;

Keep in mind that in simpler cases, .expectTo() can also be used to accomplish the same objective. Let's consider a similar problem:

interface CsvData {
  metadata: {
    count: number
  }
  entries: object[]
}

The idea is similar, but this time, in addition to validating that the data conforms to the above interface, we want to assert that the length of <csvData>.entries is equal to <csvData>.metadata.count. The solution?

const csvDataValidator = validator`{
  metadata: {
    count: number
  }
  entries: object[]
} & ${validator.expectTo(csvData => {
  if (csvData.entries.length !== csvData.metadata.count) {
    return "have an '.entries' property with a length equal to '.metadata.count'.";
  }
})}`;

We can use a .expectTo() in this scenario because it's fairly easy for the callback to access and verify the length of .entries.

Last updated