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
(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
The merge rules
- Objects merge — keys from the overlay are added to the result. Keys present in both are themselves recursively merged.
- Scalars — overlay wins. A
titlestring in the overlay replaces the basetitle. - Required arrays — the custom
arrayMergedetects 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"]. - Enum arrays — string arrays that look like enum values are merged the same way (union).
- oneOf arrays — concatenated. The overlay's
oneOfbranches are appended to the base's, not replaced. - Form-reference attributes — fields like
baspi5Ref,ntsRef,ta6Refare 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:
- Base transaction structure (participants · propertyPack · status · …)
- + BASPI v5 enrichment (priceInformation, ownership, marketing, listing details)
- + NTS Material Information 2025 (specialist issues, energy, council tax, …)
- +
baspi5Ref+nts2Refannotations on every leaf for traceability
Conveyancer product
const schema = getTransactionSchema(BASE_ID, ['ta6', 'ta7', 'ta10', 'lpe1']);
Composition:
- Base
- + TA6 Property Information Form
- + TA7 Leasehold Information Form (only relevant if leasehold sale)
- + TA10 Fittings & Contents
- + LPE1 Leasehold Property Enquiries
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.