Skip to main content

Overview

The Digital Planning Data schemas are generated from TypeScript interfaces using the ts-json-schema-generator library. This means you get full TypeScript support with type safety, autocomplete, and compile-time validation.

Type Architecture

The type definitions are organized in the /types directory:
types/
├── shared/           # Shared types used across schemas
│   ├── User.ts
│   ├── Metadata.ts
│   ├── Addresses.ts
│   ├── Contacts.ts
│   └── ...
└── schemas/          # Schema-specific types
    ├── application/
    │   ├── index.ts
    │   ├── data/
    │   │   ├── Applicant.ts
    │   │   ├── Property.ts
    │   │   ├── Proposal.ts
    │   │   └── ...
    │   └── enums/
    │       ├── ApplicationTypes.ts
    │       ├── PropertyTypes.ts
    │       └── ...
    └── ...

Importing Types

When you’ve installed the schemas as a package:
// Import root schema types
import type {Application} from 'digital-planning-data-schemas/types/schemas/application';
import type {PreApplication} from 'digital-planning-data-schemas/types/schemas/preApplication';
import type {PrototypeApplication} from 'digital-planning-data-schemas/types/schemas/prototypeApplication';
import type {Enforcement} from 'digital-planning-data-schemas/types/schemas/enforcement';

// Import specific data types
import type {Applicant} from 'digital-planning-data-schemas/types/schemas/application/data/Applicant';
import type {Property} from 'digital-planning-data-schemas/types/schemas/application/data/Property';
import type {Proposal} from 'digital-planning-data-schemas/types/schemas/application/data/Proposal';

// Import enums and constants
import type {ApplicationType} from 'digital-planning-data-schemas/types/schemas/application/enums/ApplicationTypes';
import {ApplicationTypes} from 'digital-planning-data-schemas/types/schemas/application/enums/ApplicationTypes';

// Import shared types
import type {User} from 'digital-planning-data-schemas/types/shared/User';
import type {Metadata} from 'digital-planning-data-schemas/types/shared/Metadata';

Core Schema Types

Application

The root type for planning applications:
import type {Application} from 'digital-planning-data-schemas/types/schemas/application';

/**
 * @title Application
 * @description The root specification for a planning application in England
 * generated by a digital planning service
 */
interface Application {
  data: {
    application: ApplicationData;
    user: User;
    applicant: Applicant;
    property: Property;
    proposal: Proposal;
    files?: FilesAsData;
  };
  preAssessment?: PreAssessment;
  responses: Responses;
  files: File[];
  metadata: Metadata;
}

User

Describes the role of the person who completed the application:
import type {User} from 'digital-planning-data-schemas/types/shared/User';

/**
 * @title User
 * @description The role of the user who completed the application
 */
interface User {
  role: 'applicant' | 'agent' | 'proxy';
}

Applicant

The applicant can be a direct applicant or include agent information:
import type {Applicant, BaseApplicant, Agent} from 'digital-planning-data-schemas/types/schemas/application/data/Applicant';

/**
 * @description The user who completed the application either for themself
 * or on behalf of someone else
 */
type Applicant = BaseApplicant | Agent;

/**
 * @description Information about the user who completed the application
 * for themself, or information about the person who the user applied
 * on behalf of
 */
type BaseApplicant = ContactDetails &
  SiteContact &
  MaintenanceContacts & {
    type: 'individual' | 'company' | 'charity' | 'public' | 'parishCouncil';
    address: UserAddress;
    ownership?: Ownership;
  };

/**
 * @description Information about the agent or proxy who completed
 * the application on behalf of someone else
 */
interface Agent extends BaseApplicant {
  agent: ContactDetails & {address: Address};
}

Metadata

Every application includes metadata about its source and submission:
import type {Metadata, PlanXMetadata, AnyProviderMetadata} from 'digital-planning-data-schemas/types/shared/Metadata';

type Metadata = AnyProviderMetadata | PlanXMetadata;

interface BaseMetadata {
  /** Organisation code from planning.data.gov.uk */
  organisation: string; // maxLength: 4
  /** Unique identifier for this application */
  id: UUID;
  submittedAt: DateTime;
  schema: URL;
}

interface PlanXMetadata extends BaseMetadata {
  source: 'PlanX';
  service: {
    flowId: UUID;
    url: URL;
    files: RequestedFiles;
    fee: FeeExplanation | FeeExplanationNotApplicable;
    enhancements?: Enhancements;
  };
}

Application Types

Application types are defined as a const object with type inference:
import {ApplicationTypes, ApplicationType, ApplicationTypeKeys} from 'digital-planning-data-schemas/types/schemas/application/enums/ApplicationTypes';

// The const object containing all application types
const ApplicationTypes = {
  advertConsent: 'Consent to display an advertisement',
  'ldc.existing': 'Lawful Development Certificate - Existing use',
  'ldc.proposed': 'Lawful Development Certificate - Proposed use',
  'pa.part1.classA': 'Prior Approval - Larger extension to a house',
  'pp.full.householder': 'Planning Permission - Full householder',
  'pp.full.major': 'Planning Permission - Major application',
  // ... many more types
};

// Type-safe keys
type ApplicationTypeKeys = keyof typeof ApplicationTypes;

// Structured type with value and description
type ApplicationType = {
  value: ApplicationTypeKeys;
  description: string;
};
Usage example:
import {ApplicationTypes} from 'digital-planning-data-schemas/types/schemas/application/enums/ApplicationTypes';

// Type-safe application type
const applicationType = {
  value: 'pp.full.householder' as const,
  description: ApplicationTypes['pp.full.householder'],
};

// Get description from value
function getApplicationDescription(key: ApplicationTypeKeys): string {
  return ApplicationTypes[key];
}

console.log(getApplicationDescription('ldc.existing'));
// "Lawful Development Certificate - Existing use"

Utility Types

The schemas define several utility types:
// From types/shared/utils.ts

/**
 * @format uuid
 */
type UUID = string;

/**
 * @format date-time
 */
type DateTime = string;

/**
 * @format uri
 */
type URL = string;

/**
 * @format email
 */
type Email = string;

Type Guards

Create type guards for runtime type checking:
import type {Applicant, Agent, BaseApplicant} from 'digital-planning-data-schemas/types/schemas/application/data/Applicant';

function isAgent(applicant: Applicant): applicant is Agent {
  return 'agent' in applicant;
}

function processApplicant(applicant: Applicant) {
  if (isAgent(applicant)) {
    // TypeScript knows applicant is Agent here
    console.log('Agent:', applicant.agent.name);
    console.log('Applicant:', applicant.name);
  } else {
    // TypeScript knows applicant is BaseApplicant here
    console.log('Direct applicant:', applicant.name);
  }
}

Working with Examples

The repository includes typed example data:
import type {Application} from 'digital-planning-data-schemas/types/schemas/application';

const version = process.env['VERSION'] || '@next';

export const planningPermissionFullHouseholder: Application = {
  data: {
    application: {
      type: {
        value: 'pp.full.householder',
        description: 'Planning Permission - Full householder',
      },
      fee: {
        calculated: 258,
        payable: 258,
        category: {
          sixAndSeven: 258,
        },
        exemption: {
          disability: false,
          resubmission: false,
        },
        reduction: {
          sports: false,
          parishCouncil: false,
          alternative: false,
        },
        reference: {
          govPay: 'sandbox-ref-456',
        },
      },
      declaration: {
        accurate: true,
        connection: {
          value: 'none',
        },
      },
      CIL: {
        result: 'notLiable',
        declaration: true,
        s73Application: false,
        existingPermissionPrecedingCIL: false,
        newDwellings: false,
        floorAreaHundredPlus: false,
      },
    },
    user: {
      role: 'proxy',
    },
    applicant: {
      type: 'individual',
      name: {
        first: 'David',
        last: 'Bowie',
      },
      email: '[email protected]',
      phone: {
        primary: 'Not provided by agent',
      },
      address: {
        sameAsSiteAddress: true,
      },
      siteContact: {
        role: 'proxy',
      },
      ownership: {
        interest: 'owner.sole',
        certificate: 'a',
        agriculturalTenants: false,
        declaration: {
          accurate: true,
        },
      },
      agent: {
        name: {
          first: 'Ziggy',
          last: 'Stardust',
        },
        email: '[email protected]',
        phone: {
          primary: '01100 0110 0011',
        },
        address: {
          line1: '40 Stansfield Road',
          line2: 'Brixton',
          town: 'London',
          county: 'Greater London',
          postcode: 'SW9 9RZ',
          country: 'UK',
        },
      },
    },
    // ... more data
  },
  responses: [],
  files: [],
  metadata: {
    organisation: 'LBH',
    id: '81bcaa0f-baf5-4573-ba0a-ea868c573faf',
    source: 'PlanX',
    submittedAt: '2023-10-02T00:00:00.00Z',
    schema: `https://theopensystemslab.github.io/digital-planning-data-schemas/${version}/schemas/application.json`,
    service: {
      flowId: '01e38c5d-e701-4e44-acdc-4d6b5cc3b854',
      url: 'https://www.editor.planx.dev/lambeth/apply-for-planning-permission/preview',
      files: {
        required: ['roofPlan.existing', 'roofPlan.proposed'],
        recommended: ['floorPlan.existing', 'floorPlan.proposed'],
        optional: [],
      },
      fee: {
        calculated: [
          {
            description: 'The fee to apply for planning permission to alter or extend a single home is £258.',
            policyRefs: [
              {
                text: 'UK Statutory Instruments 2023 No. 1197',
                url: 'https://www.legislation.gov.uk/uksi/2023/1197/made',
              },
            ],
          },
        ],
        payable: [
          {
            description: 'The fee to apply for planning permission to alter or extend a single home is £258.',
          },
        ],
      },
    },
  },
};

Schema Generation

The JSON schemas are generated from TypeScript types using ts-json-schema-generator:
#!/bin/bash
# From scripts/build-schema.sh

version="${VERSION:-@next}"

dirs=("application" "preApplication" "prototypeApplication")
types=("Application" "PreApplication" "PrototypeApplication")

for i in "${!dirs[@]}"; do
  dir=${dirs[$i]}
  type=${types[$i]}

  pnpm ts-json-schema-generator \
    --path "types/schemas/${dir}/*.ts" \
    --out "schemas/${dir}.json" \
    --type "$type" \
    --id "$version" \
    --no-top-ref
done

JSDoc Annotations

Types use JSDoc comments to provide schema metadata:
/**
 * @title User
 * @description The role of the user who completed the application
 */
export interface User {
  role: 'applicant' | 'agent' | 'proxy';
}

/**
 * @id #Applicant
 * @description The user who completed the application either for themself
 * or on behalf of someone else
 */
export type Applicant = BaseApplicant | Agent;

/**
 * @description Organisation code from planning.data.gov.uk
 * @maxLength 4
 */
organisation: string;

/**
 * @format uuid
 */
type UUID = string;
These annotations are converted to JSON Schema properties during generation.

TypeScript Configuration

Recommended tsconfig.json settings for working with the schemas:
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "skipLibCheck": true,
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "strict": true,
    "moduleResolution": "node",
    "types": ["node"],
    "paths": {
      "digital-planning-data-schemas/*": ["./node_modules/digital-planning-data-schemas/*"]
    }
  }
}

Best Practices

Import types using import type to avoid runtime overhead:
// Good: Type-only import (erased at runtime)
import type {Application} from 'digital-planning-data-schemas/types/schemas/application';

// Also good: Import values when needed
import {ApplicationTypes} from 'digital-planning-data-schemas/types/schemas/application/enums/ApplicationTypes';
Many types are unions - use type guards to narrow types:
type Applicant = BaseApplicant | Agent;
type Metadata = AnyProviderMetadata | PlanXMetadata;

function isPlanXMetadata(meta: Metadata): meta is PlanXMetadata {
  return meta.source === 'PlanX';
}
Derive custom types from the base schemas:
import type {Application} from 'digital-planning-data-schemas/types/schemas/application';

// Extract just the application data
type ApplicationData = Application['data']['application'];

// Extract nested types
type PropertyAddress = Application['data']['property']['address'];

// Create partial types for updates
type ApplicationUpdate = Partial<Application>;
Use types alongside runtime validation:
import type {Application} from 'digital-planning-data-schemas/types/schemas/application';
import applicationSchema from 'digital-planning-data-schemas/schemas/application.json';
import Ajv from 'ajv';

const ajv = new Ajv();
const validate = ajv.compile(applicationSchema);

function processApplication(data: unknown): Application {
  if (!validate(data)) {
    throw new Error('Invalid application data');
  }
  // TypeScript now knows data is Application
  return data as Application;
}

Next Steps

Examples

See real-world examples using TypeScript types

Schema Reference

Explore the full application schema

Build docs developers (and LLMs) love