You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
validate() on generated profiles re-derives and inlines every check (required, choice-required, references, bindings) per profile. Two problems followed from this:
Base-resource cardinality not re-stated in a profile's differential was silently dropped, so validate() skipped inherited required fields (e.g. Provenance.target, .recorded). Fixed as a stopgap in TypeSchema/TS: emit validateRequired() for inherited base-required fields #166 via a SnapshotProfileTypeSchema.inheritedRequiredFields side-list + an extra emit loop.
That stopgap only carries required-ness. The same dropped base info affects inherited choice-required, references, and bindings — each would need its own parallel side-list and emit path. The duplicated emit path also has a known limitation: a base choice declaration (value[x], 1..1) inherited but not re-stated emits validateRequired("value[x]") instead of validateChoiceRequired(...) — a check against a property that never exists in FHIR JSON.
The root issue is layering: profile validate() reads a constraint-only view but needs the effective (base ⊕ differential) view, and there's no base-resource validator to lean on.
Proposal
Generate a standalone validate<Type>(res, profileName?)free function per specialization (resource/complex-type), and have profile validators delegate to it:
// generated for the base resourceexportconstvalidateProvenance=(res,profileName="Provenance")=>({errors: [...],warnings: [...]})// generated profile classvalidate(): { errors: string[]; warnings: string[]}{constbase=validateProvenance(this.resource,"PraxisProposalProvenance")return{errors: [...base.errors, ...profileSpecificChecks],warnings: [...base.warnings]}}
Free functions (not methods) because plain generated types are interface/type with nowhere to hang a method — and because per-type runtime validation is independently useful: the type system can't enforce runtime presence, array cardinality, value/fixed constraints, large-ValueSet bindings, reference targets, or FHIRPath invariants.
The single emitter (collectRegularFieldValidation + the choice-declaration branch) is reused for both base and profile, so choice/reference/binding handling is uniform — the #166 choice-declaration bug disappears.
Semantics
Base cardinality is a non-negotiable floor. A standalone validate<Type> has no profile to relax it. Profiles only add constraints on top. This removes inheritedRequiredFields and the base-level min:0 reset entirely; intra-chain relaxation (a leaf relaxing what an intermediate profile tightened, still ≥ base floor) stays handled in the flat constraint snapshot.
Profile delegation = base() ++ profileChecks; no subtraction needed.
Scope
Opt-in flag (sibling of generateProfile, e.g. generateValidators on the writer options) — generating validators for emitted types roughly doubles output, and many consumers want types only. Emit per-type so bundlers can drop unused validators.
Top-level fields first. Nested backbone required fields and FHIRPath invariants are a deliberately separate later milestone — do not let this slide into a full structural validator in v1.
validate<Type> free function generated per specialization when the flag is on.
Profile validate() delegates to its base validator and appends only differential checks.
Inherited required, choice-required, reference, and binding constraints all validated via the shared emitter.
Behaviour-level tests in examples/ (run generated validate(), assert on errors) covering an inherited required field not re-stated by the profile.
Opt-in flag documented; default behaviour unchanged when off.
Open questions
Flag name + default (generateValidators?).
File layout: one validators.ts per package, or co-located with each type?
Should validate<Type> be emitted unconditionally for any type that has a profile in the output, even if the type itself is tree-shaken from the surface?
Follow-up to #166, which lands the targeted fix for inherited required fields. This issue tracks the broader redesign that subsumes it.
Background
validate()on generated profiles re-derives and inlines every check (required, choice-required, references, bindings) per profile. Two problems followed from this:validate()skipped inherited required fields (e.g.Provenance.target,.recorded). Fixed as a stopgap in TypeSchema/TS: emit validateRequired() for inherited base-required fields #166 via aSnapshotProfileTypeSchema.inheritedRequiredFieldsside-list + an extra emit loop.value[x],1..1) inherited but not re-stated emitsvalidateRequired("value[x]")instead ofvalidateChoiceRequired(...)— a check against a property that never exists in FHIR JSON.The root issue is layering: profile
validate()reads a constraint-only view but needs the effective (base ⊕ differential) view, and there's no base-resource validator to lean on.Proposal
Generate a standalone
validate<Type>(res, profileName?)free function per specialization (resource/complex-type), and have profile validators delegate to it:Free functions (not methods) because plain generated types are
interface/typewith nowhere to hang a method — and because per-type runtime validation is independently useful: the type system can't enforce runtime presence, array cardinality, value/fixed constraints, large-ValueSet bindings, reference targets, or FHIRPath invariants.The single emitter (
collectRegularFieldValidation+ the choice-declaration branch) is reused for both base and profile, so choice/reference/binding handling is uniform — the #166 choice-declaration bug disappears.Semantics
validate<Type>has no profile to relax it. Profiles only add constraints on top. This removesinheritedRequiredFieldsand the base-levelmin:0reset entirely; intra-chain relaxation (a leaf relaxing what an intermediate profile tightened, still ≥ base floor) stays handled in the flat constraint snapshot.base() ++ profileChecks; no subtraction needed.Scope
generateProfile, e.g.generateValidatorson the writer options) — generating validators for emitted types roughly doubles output, and many consumers want types only. Emit per-type so bundlers can drop unused validators.What this replaces
SnapshotProfileTypeSchema.inheritedRequiredFields(introduced in TypeSchema/TS: emit validateRequired() for inherited base-required fields #166)profile-validation.tsmin:0reset inflatProfileinheritedRequiredFieldsequality) — superseded by behaviour-level tests that runvalidate().Acceptance criteria
validate<Type>free function generated per specialization when the flag is on.validate()delegates to its base validator and appends only differential checks.examples/(run generatedvalidate(), assert onerrors) covering an inherited required field not re-stated by the profile.Open questions
generateValidators?).validators.tsper package, or co-located with each type?validate<Type>be emitted unconditionally for any type that has a profile in the output, even if the type itself is tree-shaken from the surface?Follow-up to #166, which lands the targeted fix for inherited required fields. This issue tracks the broader redesign that subsumes it.