Implementation Updated 2026-05-14

Schema composition

How getTransactionSchema(schemaId, overlays[]) builds the merged schema you actually validate against. This is the most-touched API surface in the package; understanding the merge rules saves a lot of debugging.

The merge pipeline

flowchart LR classDef src fill:#eef4f8,stroke:#1a4d80,color:#0b2545; classDef proc fill:#fef3c7,stroke:#b45309,color:#7c2d12; classDef out fill:#dcfce7,stroke:#166534,color:#14532d; BASE["pdtf-transaction.json
(base)"]:::src O1[Overlay 1]:::src O2[Overlay 2]:::src ON[Overlay n…]:::src M["deepmerge with custom arrayMerge"]:::proc R["Merged schema
(validated against)"]:::out BASE --> M O1 --> M O2 --> M ON --> M M --> R
Each overlay is deep-merged into the running result in the order given.

The merge rules

  1. Objects merge — keys from the overlay are added to the result. Keys present in both are themselves recursively merged.
  2. Scalars — overlay wins. A title string in the overlay replaces the base title.
  3. Required arrays — the custom arrayMerge detects field-name arrays (string entries that look like JSON property names) and unions them. So if base requires ["a", "b"] and overlay requires ["b", "c"], the result requires ["a", "b", "c"].
  4. Enum arrays — string arrays that look like enum values are merged the same way (union).
  5. oneOf arrays — concatenated. The overlay's oneOf branches are appended to the base's, not replaced.
  6. Form-reference attributes — fields like baspi5Ref, ntsRef, ta6Ref are copied through as ordinary scalar properties (with normal "overlay wins" semantics).

The custom array-merge logic is the only non-obvious part — see index.js around line 100 for the implementation.

Worked examples

Estate-agency product

const schema = getTransactionSchema(BASE_ID, ['baspi5', 'nts2']);

Composition:

Conveyancer product

const schema = getTransactionSchema(BASE_ID, ['ta6', 'ta7', 'ta10', 'lpe1']);

Composition:

Staged NTS → NTS2 migration

If you already produce NTS-2023 data and want to add just Japanese knotweed coverage from NTS2:

const schema = getTransactionSchema(BASE_ID, ['nts2023', 'jk']);

Same with transfer fees:

const schema = getTransactionSchema(BASE_ID, ['nts2023', 'jk', 'tf']);

The 16 extension overlays exist exactly for this — see Extension overlays.

Order matters

Overlays are merged in array order. Where two overlays both say something about the same field, the later overlay wins for scalar fields. For required and enum arrays the order doesn't matter (set union), but for titles, descriptions and discriminator-driven oneOf branches it does.

Convention is to put more-specific overlays later. For the conveyancer case above, putting ta6 last (e.g. ['lpe1','ta10','ta7','ta6']) would let TA6 override anything TA7/TA10/LPE1 set for shared fields — usually not what you want.

Names & aliases

Overlays are addressable by either their filename stem ('baspi5', 'ta6') or the versioned alias used in overlaysMap ('baspiV5', 'ta6ed4'). The full alias table is on the PDTF overlays page.

Caching

The package internally caches merged schemas and Ajv validators per (schemaId, overlay-key) tuple. So calling getTransactionSchema(BASE, ['baspi5','ta6']) twice returns the same merged object both times. Cache stats are exposed via require('@pdtf/schemas').cacheStats.