The validator library works by traversing the JSON object provided to it, and applying rules at each level in the tree.
Every message that can be returned by the validator library is defined within a rule.
There are 2 main types of rule that the library makes use of.
These rules are applied to check that the raw input provided is a valid JSON object that is worth validating.
They are applied once at the beginning of the validation run to the entire data object.
These rules currently:
- Error if the submitted data is not an object
- Notifies of the library's limitations if the submitted data is detected to be an RPDE feed
These rules are processed at each node in the object tree, in the order that they are defined in the core rules list.
For each object, the following algorithm is followed:
- Apply all model-level rules to the whole object
- Apply all field-level rules to each field in the current object
- Iterate through each field and recursively apply ApplyRules if an
objectorArray of objectsis found within it
The constructors of both sets of rules are exported by the application in the defaultRules property:
const { defaultRules } = require('openactive-data-model-validator');
console.log(defaultRules);
// {
// "raw": [
// ...
// ],
// "core": [
// ...
// ]
// }To add a new rule, you will need to extend the Rule class.
The rule name you create should:
- Be unique
- Be written in UpperCamelCase
- End in Rule - e.g.
CheckForFooRule - Have a filename that matches the rule name in kebab case - e.g.
check-for-foo-rule.js - Have a test file that sits beside the rule file, with a trailing
-spec- e.g.check-for-foo-rule-spec.js
- Set
this.targetModelsto an array of the names of model you are targeting, or a string'*'wildcard. If you target a model, you MUST implementvalidateModel.
OR
- Set
this.targetFieldsto an object map of the fields you are targeting in each model, or a string'*''wildcard. Setting the property tonullmeans that the rule will be applied once to the whole model. If you target a field, you MUST implementvalidateField. field Generally speaking, you SHOULD NOT implement bothvalidateModelandvalidateFieldin the same rule.
Independently, a rule can also target particular modes of use. It is used to restrict rules which should only apply during a particular usage of the models (e.g. an Order used during one of the booking phases - C1Request, C2Response or PatchOrder). By default, a rule will target all modes.
There is a lot of flexibility in the way that you can target rules.
To target all models at the top level:
this.targetModels = '*';To target all fields in every model:
this.targetFields = '*';To target specific models at the top level:
this.targetModels = ['Event', 'Place'];To target specific fields in specific models:
this.targetFields = {
'Event': [
'startDate',
'endDate'
],
'Place': [
'url'
]
};To target all modes:
this.targetValidationModes = '*';To target specific modes:
this.targetValidationModes = ['C1Request', 'C2Response'];Set this.meta to explain what the rule is testing for.
Defining this detail here makes it easier for libaries to scrape all of the rules that the validator will run.
This meta object should include:
- A
namematching the name of the class. - A
descriptionexplain what the whole rule tests for. - A hash of
tests, explaining each scenario that this rule covers, and defining the message and error parameters that are thrown. Eachkeyshould be unique, but can be any value. Thiskeycan be referred to within the rule to trigger a particular error. Each test can define:description- A description of the test. Optional if there is only one test.message- The error message that is returned if this test fails. This can contain simple{{handlebars}}variable placeholders.sampleValues- If themessagecontains handlebar placeholders, this property should contain a map of sample values for documentation purposes.category- The category of the error when returned. Should come from theValidationErrorCategoryenum, and be one of:ValidationErrorCategory.CONFORMANCEValidationErrorCategory.DATA_QUALITYValidationErrorCategory.INTERNAL
severity- The category of the error when returned. Should come from theValidationErrorSeverityenum, and be one of:ValidationErrorSeverity.FAILUREValidationErrorSeverity.WARNINGValidationErrorSeverity.NOTICEValidationErrorSeverity.SUGGESTION
type- The type of the error when returned. Should come from theValidationErrorTypeenum.
this.meta = {
name: 'FieldsNotInModelRule',
description: 'Validates that all fields are present in the specification.',
tests: {
noExperimental: {
description: 'Raises a notice if experimental fields are detected.',
message: 'The validator does not currently check experimental fields.',
category: ValidationErrorCategory.CONFORMANCE,
severity: ValidationErrorSeverity.NOTICE,
type: ValidationErrorType.EXPERIMENTAL_FIELDS_NOT_CHECKED,
},
typoHint: {
description: 'Detects common typos, and raises a warning informing on how to correct.',
message: 'Field "{{typoField}}" is a common typo for "{{actualField}}". Please correct this field to "{{actualField}}".',
sampleValues: {
typoField: 'offer',
actualField: 'offers',
},
category: ValidationErrorCategory.CONFORMANCE,
severity: ValidationErrorSeverity.FAILURE,
type: ValidationErrorType.FIELD_COULD_BE_TYPO,
},
inSchemaOrg: {
description: 'Raises a notice that fields in the schema.org schema that aren\'t in the OpenActive specification aren\'t checked by the validator.',
message: 'This field is declared in schema.org but this validator is not yet capable of checking whether they have the right format or values. You should refer to the schema.org documentation for additional guidance.',
category: ValidationErrorCategory.CONFORMANCE,
severity: ValidationErrorSeverity.NOTICE,
type: ValidationErrorType.SCHEMA_ORG_FIELDS_NOT_CHECKED,
},
notInSpec: {
description: 'Raises a warning for fields that aren\'t in the OpenActive specification, and that aren\'t caught by other rules.',
message: 'This field is not defined in the OpenActive specification.',
category: ValidationErrorCategory.CONFORMANCE,
severity: ValidationErrorSeverity.WARNING,
type: ValidationErrorType.FIELD_NOT_IN_SPEC,
},
},
};Only one of these methods is expected to be implemented on each rule.
Both methods take the current node in the tree as a parameter, and return an array of zero or more ValidationErrors.
The parameters they accept are:
node- AModelNodeobject, representing the current place in the object tree. It contains the following properties:name- The name of the field that this node has come from. Will be"$"if this is the root node.arrayIndex- If this object is within an array, this field will contain the array index that it is at.value- The value object of the current node.model- The model object definition corresponding to this node. Will never benull, but could contain no constraints if the model of this node is not known to the library.options- A copy of the options passed to thevalidatemethod.parentNode- TheModelNodeobject representing the parent of this node. Will benullif this is the root node.rootNode- TheModelNodeobject representing the root node. Will benullif this is the root node.
field(validateField only) - the name of the field that has been selected for validation in this rule.
The validate* methods should use the node provided to verify the data conforms to the rules it defines.
If the node does not comply, an error can be raised by calling the createError method, inherited from the Rule object.
The createError method accepts the following parameters:
key- The key of the object defined inthis.meta.teststhat the defines the error that has been triggered.extra- An object of extra properties to pass to theValidationErrorconstructor. These are typically:value- The value of the property that has triggered the error.path- The jsonpath to the property that has triggered the error. The full path to the current node can be found by callingnode.getPath(). If you callnode.getPath()with arguments, these will be appended to the current path.
messageValues- An optional object of parameters to replace in the error message. This MUST be provided if the message has placeholders within it.
this.createError(
'default',
{
value: testValue,
path: node.getPath(field),
},
{
field,
model: node.model.type,
},
);- You should write a test for your rule.
- Add the rule's file, as well as its test file to tsconfig.json's
includearray, so that TypeScript can check for errors. - Add the rule to the list in
rules/index, so that it is processed.
TODO
const {
Rule,
ValidationError,
ValidationErrorCategory,
ValidationErrorType,
ValidationErrorSeverity
} = require('openactive-data-model-validator');
class RequiredFieldsRule extends Rule {
constructor(options) {
super(options);
this.targetModels = '*';
this.targetValidationModes = '*';
this.meta = {
name: 'RequiredFieldsRule',
description: 'Validates that all required fields are present in the JSON data.',
tests: {
default: {
message: 'Required field "{{field}}" is missing from "{{model}}".',
sampleValues: {
field: 'name',
model: 'Event',
},
category: ValidationErrorCategory.CONFORMANCE,
severity: ValidationErrorSeverity.FAILURE,
type: ValidationErrorType.MISSING_REQUIRED_FIELD,
},
},
};
}
validateModel(node) {
// Don't do this check for models that we don't actually have a spec for
if (!node.model.hasSpecification) {
return [];
}
const errors = [];
for (const field of node.model.requiredFields) {
const testValue = node.getValueWithInheritance(field);
if (typeof testValue === 'undefined') {
errors.push(
this.createError(
'default',
{
value: testValue,
path: node.getPath(field),
},
{
field,
model: node.model.type,
},
),
);
}
}
return errors;
}
}