Integrations - Validators
Location: api/nextango/apps/integrations/sync/services/
Last Updated: 2026-07-06
Overview
Two validators guard the Sanity sync boundary. The outbound reference-shape validator inspects every document the sync layer sends to Sanity and blocks a push that carries a malformed reference. The schema-to-data parity classifier locks each Sanity schema field on a Django-mirrored document type to a stated disposition, so a new schema field cannot appear without a decision about whether Django populates it.
Both exist because Django cannot introspect the JavaScript Sanity schema at runtime. The shape validator works from the emitted document plus a small declaration overlay; the parity classifier reads the schema .js files directly and compares them against a hand-authored classification map.
Outbound reference-shape validator
validate_outbound_refs(document, sanity_type, *, full_doc=True) returns a list of ShapeFinding for one outbound Sanity document. It is pure and side-effect free: it reads the document and reports anomalies, it never mutates the document. sanity_type=None runs the structural checks only; full_doc=False (a partial patch or set) skips the required-path check, because a partial write does not assert the whole document.
Source: sync/services/ref_shape_validator.py.
What it checks
Each finding carries a flag_class, the dotted path where it occurred, and a human-readable detail. The validator runs a structural walk over the whole document and then a declaration-overlay pass keyed on sanity_type.
| Flag class | Meaning | Severity |
|---|---|---|
typeless_ref | A dict carries _ref but not _type: 'reference', so it is not a valid Sanity reference | Hard |
missing_key | A reference dict inside an array has no _key; Sanity requires _key on array items | Hard |
raw_object | A non-JSON value (a Django model instance, anything with _meta) sits where a scalar or reference is expected | Hard |
bare_string_ref | A bare string sits amongst reference dicts, or in a known reference field | Hard |
required_path_missing | A schema-required reference path declared in the overlay is absent from a full document | Hard |
weak_on_strong | A strong reference (no _weak) sits in an array whose target may not yet exist in Sanity | Advisory |
pk_key_advisory | An array _key is all digits, so it looks like a Django primary key rather than a Sanity-stable key | Advisory |
Hard findings block an outbound push. Advisory findings inform only; they cover the async-ordering weak-reference case and the heuristic primary-key _key, neither of which is a reliable block signal.
The declaration overlay
The structural walk catches an anomaly that has a structural tell, such as a reference dict missing _key or a raw model instance. It cannot catch a lone bare string in a field that is a reference by schema declaration, or a schema-required reference that was silently dropped. The overlay closes that gap.
REF_FIELD_OVERLAY maps each sanity_type to its known reference paths. Each entry declares the expected shape (ref for a single reference, ref_array for an array), whether array members require _key, and whether the path is schema-required. Declared types include storeCredit, productType, inventoryLevel, promoCode, promotion, promotionalCampaign, and store. For example, storeCredit.customerId is declared a required single reference, so a full storeCredit document that omits it produces a required_path_missing finding.
Where it runs, and what callers see
The Sanity client validates before every network write. _observe_ref_shape runs on create_document, create_or_replace_document, push_update, create_if_not_exists_and_set, and batch_create_or_replace. It does two things:
- Records findings. Any finding, hard or advisory, writes a non-failing
SyncAuditLogrow withstatus='shape_warning',direction='outbound', and apayloadcarrying the findings undershape_findings. This row sits alongside the write it describes and never flips a sync tofailed. See SyncAuditLog. - Blocks a hard finding. When any hard finding is present and
settings.SANITY_REF_SHAPE_ENFORCEis on, the client raisesOutboundRefShapeErrorbefore the network call, so a malformed document never reaches Sanity. The exception carries the document type, the Sanity ID, and the hard findings.
SANITY_REF_SHAPE_ENFORCE defaults to on. Set it to False to disable the block and fall back to warn-only behaviour; the shape_warning rows are still written. A validator or logging error is swallowed, so a bug in the validator never breaks a sync write; only a real hard finding raises.
For a full document write, full_doc=True runs the required-path check. For a partial patch or set (push_update, create_if_not_exists_and_set), full_doc=False skips it, since a partial write legitimately omits paths.
Schema-to-data parity classification
sync/services/schema_parity.py locks each Sanity schema field on a Django-mirrored document type to one classification. The guard asserts that every schema field is classified, so a new schema field with no classification fails the guard and forces a decision.
Classifications
| Classification | Meaning |
|---|---|
Populated | A Django source emits this schema field directly. Carries the emit locus. |
PopulatedViaContainedSeam | An agnostic schema field populated from a vertical-contained model's regulated-universal base. It emits only agnostic vocabulary, so vertical-specific fields never leak. Carries the emit locus and the source. |
IntentionallyAbsent | No agnostic Django emit by design: born in Studio, vertical-only with no agnostic shape, or derived and read-only. Carries the reason. |
GenuineGap | Django holds the data but the transform does not emit it. Tracked residue with a stated consequence. |
The extractor and the guard
extract_schema_fields(schema_dir, document_type, follow=None) returns the dotted schema-field paths for a document type. It reads the field identifiers from the document's .js schema file. For a field named in follow, it descends into that object type's schema and appends <field>.<subfield> paths.
Recursion is keyed on the emit mechanism, not on whether a field is an object. When the transform rebuilds an object field by field, a subfield can be silently omitted, so the extractor follows those object types. When the transform copies a whole JSONField structure wholesale, every subfield Django holds is emitted structurally, so those containers are classified at the container level and are not followed.
The durable lock is the assertion extract_schema_fields(...) <= parity_map.keys(). A schema field with no classification fails it. find_studio_schema_dir locates the studio/schemaTypes tree; the api container mounts only ./api, so a local run without the studio tree skips the schema-anchor assertion, while continuous integration runs the whole tree and enforces the lock in the merge gate.
Store parity
The store document type is classified end to end, top-level fields and the nested settings and regulatedCompliance objects. The regulatedCompliance block is PopulatedViaContainedSeam: its four agnostic keys come from the store's regulated-store profile through the compliance seam, and no vertical-specific field leaks into the emitted document.
Three fields are tracked as GenuineGap: settings.pickupEnabled, settings.returnToInventory, and settings.payAtStoreEnabled. Django holds these on the Store.settings JSONField and reads them through model properties, but the outbound transform rebuilds the settings object from scalar fields and omits them. Because the outbound write replaces the whole settings object, an outbound store sync drops any Studio-authored value for these three keys. The classification records the gap and its consequence; closing it is separate work.
Related Documentation
- Models - SyncAuditLog, where
shape_warningandquarantinedrows land - Reference Logic - reference generation and dual-shape resolution
- Services - the sync client and reference resolver
- Adapters - domain sync adapters that build the outbound documents