Overview
The Digital Planning Data Schemas use JSON Schema to provide formal validation rules. This ensures data integrity and consistency across different planning systems.
Validation Libraries
The schemas are tested with two popular JSON Schema validation libraries:
AJV Fast, standard-compliant JSON Schema validator
jsonschema Simple, easy-to-use validator for Node.js
Both libraries are used in the test suite to ensure broad compatibility.
Using AJV
AJV (Another JSON Schema Validator) is a fast, feature-rich validator.
Installation
npm install ajv ajv-formats
Basic Usage
import Ajv from 'ajv' ;
import addFormats from 'ajv-formats' ;
import prototypeApplicationSchema from '../schemas/prototypeApplication.json' ;
// addFormats() is required for types like UUID, email, datetime, etc.
const ajv = addFormats ( new Ajv ({ allowUnionTypes: true }));
// Compile the schema
const validate = ajv . compile ( prototypeApplicationSchema );
// Validate data
const isValid = validate ( applicationData );
if ( ! isValid ) {
console . error ( 'Validation errors:' , validate . errors );
} else {
console . log ( 'Data is valid!' );
}
Configuration Options
Required for the Digital Planning Data Schemas, which use TypeScript union types extensively. const ajv = new Ajv ({ allowUnionTypes: true });
Error Handling
AJV provides detailed error information:
const isValid = validate ( data );
if ( ! isValid ) {
validate . errors ?. forEach ( error => {
console . error ({
path: error . instancePath ,
message: error . message ,
params: error . params ,
schemaPath: error . schemaPath
});
});
}
{
"instancePath" : "/data/applicant/email" ,
"schemaPath" : "#/properties/data/properties/applicant/properties/email/format" ,
"keyword" : "format" ,
"params" : { "format" : "email" },
"message" : "must match format \" email \" "
}
Using jsonschema
The jsonschema library provides a simpler API.
Installation
Basic Usage
import { Validator , Schema } from 'jsonschema' ;
import prototypeApplicationSchema from '../schemas/prototypeApplication.json' ;
const validator = new Validator ();
// Validate data
const result = validator . validate (
applicationData ,
prototypeApplicationSchema as Schema
);
if ( result . errors . length > 0 ) {
console . error ( 'Validation errors:' , result . errors );
} else {
console . log ( 'Data is valid!' );
}
Error Handling
const result = validator . validate ( data , schema );
result . errors . forEach ( error => {
console . error ({
property: error . property ,
message: error . message ,
name: error . name ,
argument: error . argument
});
});
Test Suite
The repository includes comprehensive validation tests:
import { describe , expect , test } from 'vitest' ;
const schemas = [
{
name: 'Application' ,
schema: applicationSchema ,
examples: getJSONExamples < Application >( 'application' ),
},
{
name: 'PreApplication' ,
schema: preApplicationSchema ,
examples: getJSONExamples < PreApplication >( 'preApplication' ),
},
{
name: 'PrototypeApplication' ,
schema: prototypeApplicationSchema ,
examples: getJSONExamples < PrototypeApplication >( 'prototypeApplication' ),
},
{
name: 'Enforcement' ,
schema: enforcementSchema ,
examples: getJSONExamples < Enforcement >( 'enforcement' ),
},
];
describe . each ( schemas )( '$name' , ({ schema , examples }) => {
const validator = new Validator ();
const ajv = addFormats ( new Ajv ({ allowUnionTypes: true }));
const validate = ajv . compile ( schema );
describe . each ( examples )(
'$data.application.type.description || $applicationType' ,
example => {
test ( 'accepts a valid example' , () => {
const result = validator . validate ( example , schema as Schema );
expect ( result . errors ). toHaveLength ( 0 );
const isValid = validate ( example );
expect ( validate . errors ). toBeNull ();
expect ( isValid ). toBe ( true );
});
test ( 'rejects an invalid example' , () => {
const invalidExample = { foo: 'bar' };
const result = validator . validate ( invalidExample , schema as Schema );
expect ( result . errors ). not . toHaveLength ( 0 );
const isValid = validate ( invalidExample );
expect ( validate . errors ). not . toBeNull ();
expect ( isValid ). toBe ( false );
});
}
);
});
Running Tests
# Build schemas and run tests
pnpm test
# Build schemas only
pnpm build-schema
# Run tests only (requires schemas to be built first)
vitest
Validation Patterns
Required vs Optional Fields
JSON Schema distinguishes between required and optional fields:
// TypeScript with optional property
interface Applicant {
name : {
first : string ;
last : string ;
};
email : string ;
phone ?: { // Optional
primary : string ;
};
}
This generates a schema with required fields:
{
"type" : "object" ,
"properties" : {
"name" : { "$ref" : "#/definitions/Name" },
"email" : { "type" : "string" , "format" : "email" },
"phone" : { "$ref" : "#/definitions/Phone" }
},
"required" : [ "name" , "email" ]
}
Common format validations:
// UUID validation
id : UUID ; // format: "uuid"
// Email validation
email : string ; // format: "email" (via JSDoc)
// Date validation
submittedAt : DateTime ; // format: "date-time"
receivedDate : Date ; // format: "date"
// URL validation
schema : URL ; // format: "uri"
Enum Validation
TypeScript enums become JSON Schema enums:
type UserRole = 'applicant' | 'agent' | 'proxy' ;
interface User {
role : UserRole ;
}
Generates:
{
"properties" : {
"role" : {
"type" : "string" ,
"enum" : [ "applicant" , "agent" , "proxy" ]
}
}
}
Array Validation
Arrays can specify item types:
interface Application {
files : File []; // Array of File objects
responses : Responses ; // Array type alias
}
Conditional Types
The schemas support discriminated unions:
type PrototypeApplication =
| LawfulDevelopmentCertificateExisting
| PriorApprovalPart1ClassA
| PlanningPermissionFullHouseholder ;
This generates a schema with anyOf allowing any of the types.
Common Validation Errors
Missing Required Field
{
"instancePath" : "/data/applicant" ,
"message" : "must have required property 'email'"
}
{
"instancePath" : "/metadata/id" ,
"message" : "must match format 'uuid'"
}
Type Mismatch
{
"instancePath" : "/data/application/fee/calculated" ,
"message" : "must be number"
}
Invalid Enum Value
{
"instancePath" : "/data/user/role" ,
"message" : "must be equal to one of the allowed values" ,
"params" : { "allowedValues" : [ "applicant" , "agent" , "proxy" ] }
}
Direct Schema Validation
You can validate against hosted schemas directly:
import Ajv from 'ajv' ;
import addFormats from 'ajv-formats' ;
import fetch from 'node-fetch' ;
const ajv = addFormats ( new Ajv ({ allowUnionTypes: true }));
// Fetch schema from GitHub Pages
const schemaUrl =
'https://theopensystemslab.github.io/digital-planning-data-schemas/0.7.7/schemas/prototypeApplication.json' ;
const response = await fetch ( schemaUrl );
const schema = await response . json ();
// Compile and validate
const validate = ajv . compile ( schema );
const isValid = validate ( applicationData );
TypeScript Type Checking
For TypeScript projects, you get compile-time type checking:
import type { PrototypeApplication } from 'digital-planning-data-schemas/types/schemas/prototypeApplication' ;
// TypeScript will enforce the correct structure
const application : PrototypeApplication = {
applicationType: 'pp.full.householder' ,
data: {
user: { role: 'applicant' },
applicant: { /* ... */ },
application: { /* ... */ },
property: { /* ... */ },
proposal: { /* ... */ }
},
responses: [],
files: [],
metadata: { /* ... */ }
};
// TypeScript error if structure is wrong:
// const invalid: PrototypeApplication = {
// wrong: 'structure' // Error: Object literal may only specify known properties
// };
TypeScript provides compile-time validation, while JSON Schema provides runtime validation. Use both for maximum safety.
Custom Validation
You can extend validation with custom rules:
import Ajv from 'ajv' ;
import addFormats from 'ajv-formats' ;
const ajv = addFormats ( new Ajv ({ allowUnionTypes: true }));
// Add custom keyword
ajv . addKeyword ({
keyword: 'isValidUPRN' ,
validate : ( schema : any , data : any ) => {
// UPRN must be 12 digits
return / ^ \d {12} $ / . test ( data );
}
});
// Add custom format
ajv . addFormat ( 'uk-postcode' , / ^ [ A-Z ] {1,2} \d {1,2} [ A-Z ] ? \s ? \d [ A-Z ] {2} $ / i );
Validation in CI/CD
The repository includes automated validation in the CI pipeline:
.github/workflows/test.yml
name : Test
on : [ push , pull_request ]
jobs :
test :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v3
- uses : pnpm/action-setup@v2
- uses : actions/setup-node@v3
with :
node-version : '18'
cache : 'pnpm'
- run : pnpm install
- run : pnpm test
This ensures:
Schemas are valid JSON Schema
All examples validate against schemas
No regressions are introduced
Best Practices
Always validate user input
Compiling schemas is expensive. Cache the compiled validator: // Good: Compile once
const validate = ajv . compile ( schema );
app . post ( '/applications' , ( req , res ) => {
const isValid = validate ( req . body ); // Reuse compiled validator
// ...
});
// Bad: Compile on every request
app . post ( '/applications' , ( req , res ) => {
const validate = ajv . compile ( schema ); // Wasteful
// ...
});
Use TypeScript types alongside validation
Combine compile-time and runtime validation: import type { PrototypeApplication } from 'digital-planning-data-schemas' ;
function processApplication ( data : unknown ) {
// Runtime validation
if ( ! validate ( data )) {
throw new Error ( 'Invalid data' );
}
// Now TypeScript knows the type
const app = data as PrototypeApplication ;
console . log ( app . applicationType ); // Type-safe access
}
Use the example files from the repository as test fixtures: import fullHouseholder from 'digital-planning-data-schemas/examples/prototypeApplication/planningPermission/fullHouseholder.json' ;
test ( 'validates householder example' , () => {
expect ( validate ( fullHouseholder )). toBe ( true );
});
When building applications, always reference a specific schema version: const metadata = {
schema: 'https://theopensystemslab.github.io/digital-planning-data-schemas/0.7.7/schemas/prototypeApplication.json' ,
// ...
};
This ensures consistency even as schemas evolve.
Next Steps
API Implementation Learn how to implement APIs using these schemas
Examples Browse example applications for each schema type