Skip to main content

Validating JSON data against a Model

Concerto provides several ways to validate that a JSON document conforms to a model. The recommended path — added in 4.x — is the validateInstance API on ModelManager and ClassDeclaration. It wraps the existing Serializer.fromJSON + ResourceValidator pipeline so you do not have to assemble it by hand or use try/catch for control flow.

Use this when you have a JSON object and want to know whether it conforms to a model — and, if not, what's wrong.

const { ModelManager } = require('@accordproject/concerto-core');

const mm = new ModelManager();
mm.addCTOModel(`namespace test@1.0.0

concept Driver {
o String firstName
o String lastName
o String favoriteColor
}

concept Car identified by vin {
o String vin
o Driver owner
}`);

const data = {
$class: 'test@1.0.0.Car',
vin: 'abc123',
owner: {
$class: 'test@1.0.0.Driver',
firstName: 'John',
lastName: 'Doe',
favoriteColor: 'Blue',
},
};

const result = mm.validateInstance(data);

if (result.valid) {
console.log('Valid! Resource:', result.resource.toString());
} else {
for (const d of result.errors) {
console.log(`[${d.code}] ${d.path}: ${d.message}`);
}
}

ModelManager#validateInstance resolves the type from the JSON's $class discriminator. If you already hold the expected ClassDeclaration, call it directly — same signature, with the added benefit of also validating that the input's $class is the expected type (or a subtype):

const carDecl = mm.getType('test@1.0.0.Car');
const result = carDecl.validateInstance(data);

The shape of the result

validateInstance returns a discriminated ValidationResult:

type ValidationResult =
| { valid: true; resource: Resource; warnings: Diagnostic[] }
| { valid: false; resource: null; errors: Diagnostic[]; warnings: Diagnostic[] };

type Diagnostic = {
code: string; // e.g. 'MISSING_REQUIRED_FIELD', 'TYPE_MISMATCH'
message: string; // human-readable; do not parse
path: string; // JSON Pointer to the offending value, e.g. '/owner/email'
expected?: string;
actual?: string;
severity: 'error' | 'warning';
cause?: Error;
};

Diagnostic codes are stable and machine-readable:

CodeWhen
TYPE_MISMATCHThe JSON $class does not match the expected type (or any subtype)
MISSING_CLASSThe JSON object has no $class field
UNKNOWN_TYPEThe JSON $class is not registered in the ModelManager
MISSING_REQUIRED_FIELDA non-optional field is absent
UNDECLARED_FIELDThe JSON has a property not declared on the model
FIELD_TYPE_VIOLATIONA field's value is of the wrong primitive type
VALIDATOR_VIOLATIONA field value fails its declared regex or range constraint
INVALID_ENUM_VALUEA value is not a member of the declared enum
INVALID_FIELD_ASSIGNMENTA nested object's type is not assignable to its field
NOT_RELATIONSHIPA relationship slot holds something that is not a relationship
INVALID_RELATIONSHIPA relationship points to a non-identifiable type
ABSTRACT_CLASSThe instance is declared as an abstract type
EMPTY_IDENTIFIERAn identified instance has an empty identifier
DESERIALIZATION_ERRORThe JSON could not be deserialised at all (see Aggregation below)

Aggregation

By default, all post-deserialization violations are collected. Pass { collectAll: false } for first-error semantics:

const result = mm.validateInstance(data, { collectAll: false });

Aggregation is best-effort. The JSON populator runs first; if it rejects the input outright (for example a number where a string field is expected, or extra unknown properties), you get a single DESERIALIZATION_ERROR. Once deserialization succeeds, all remaining violations (missing required fields, regex or range violations, invalid enum values, abstract instantiation, relationship type mismatches, etc.) are aggregated.

Convenience forms

If you don't need the diagnostic list and prefer exceptions:

try {
const car = mm.validateInstanceOrThrow(data);
// `car` is a fully-populated Resource ready to use
} catch (err) {
// err is a ValidationException with err.diagnostics: Diagnostic[]
}

If you just want a boolean:

if (mm.isValidInstance(data)) {
// ...
}

Both are also available on ClassDeclaration.

Generating a sample instance

ClassDeclaration#generateSample produces a JSON object that conforms to the type — handy for tests, documentation, or seeding form values:

const sample = carDecl.generateSample({ includeOptionalFields: true });
console.log(JSON.stringify(sample, null, 2));

Sample data round-trips: decl.validateInstance(decl.generateSample()).valid is always true.

Worked example: surfacing form errors

function validateCarForm(json) {
const result = mm.getType('test@1.0.0.Car').validateInstance(json);
if (result.valid) {
return { ok: true, car: result.resource };
}
// Group diagnostics by field path so the UI can render inline errors
const byField = {};
for (const d of result.errors) {
(byField[d.path] ||= []).push(d.message);
}
return { ok: false, fieldErrors: byField };
}

Lower-level alternative: Serializer.fromJSON

Serializer.fromJSON is still available and remains the underlying primitive. It throws on the first violation and returns a populated Resource on success:

try {
const resource = mm.getSerializer().fromJSON(data);
resource.validate(); // re-runs the validator over the populated resource
console.log('Valid');
} catch (err) {
console.log(err.message);
}

Reach for the serializer directly only when you specifically need its options (e.g. convertResourcesToRelationships, deduplicateResources) that the higher-level API does not expose. For everyday "is this JSON valid against this model?", prefer validateInstance.

Introspecting the model

ModelManager.getType returns a ClassDeclaration you can introspect at runtime:

const decl = mm.getType('test@1.0.0.Car');
console.log(decl.getFullyQualifiedName());
decl.getProperties().forEach(p => {
console.log(`- ${p.getName()} : ${p.getFullyQualifiedTypeName()}`);
});

These introspection APIs let you examine declared properties, super types, and meta-properties for any modelled type.