diff --git a/src/typeschema/utils.ts b/src/typeschema/utils.ts index c457163a..2bc9ca29 100644 --- a/src/typeschema/utils.ts +++ b/src/typeschema/utils.ts @@ -619,6 +619,13 @@ export const mkTypeSchemaIndex = ( if (nonConstraintSchema?.fields) { for (const [name, baseField] of Object.entries(nonConstraintSchema.fields)) { if (!baseField.required) continue; + // Choice declarations (e.g. `value[x]`) are required via the + // declaration, not a real property — validateRequired() would + // check a key that never exists in FHIR JSON. The correct + // validateChoiceRequired() handling is deferred to the per-type + // validator redesign (#169); until then, skip rather than emit + // a check that can only misfire. + if (isChoiceDeclarationField(baseField)) continue; const flat = flatFields[name] as { required?: boolean; min?: number } | undefined; // Profile explicitly relaxed the field via differential min:0 → // skip (regular validate emission also skips it because flatField diff --git a/test/unit/typeschema/utils.test.ts b/test/unit/typeschema/utils.test.ts index 966f1e88..20f024ee 100644 --- a/test/unit/typeschema/utils.test.ts +++ b/test/unit/typeschema/utils.test.ts @@ -740,5 +740,45 @@ describe("TypeSchema Index", () => { expect(snap.inheritedRequiredFields).toEqual(["target", "recorded", "agent"]); }); + + it("skips a base-required choice declaration the profile does not re-state", () => { + // A required choice (`value[x]`, 1..1) is satisfied via one of its + // variants, not a property literally named "value[x]". Listing it in + // inheritedRequiredFields would emit validateRequired("value[x]") — + // a check against a key that never exists in FHIR JSON. It must be + // skipped (proper validateChoiceRequired handling is tracked in #169). + const base: SpecializationTypeSchema = { + identifier: { + name: "Observationish" as Name, + package: "test", + kind: "resource", + version: "1.0.0", + url: "http://example.org/StructureDefinition/Observationish" as CanonicalUrl, + }, + fields: { + recorded: { type: stringType, required: true, array: false }, + "value[x]": { choices: ["valueString", "valueQuantity"], required: true }, + }, + }; + const profile: ProfileTypeSchema = { + identifier: { + name: "ObservationishProfile" as Name, + package: "test", + kind: "profile", + version: "1.0.0", + url: "http://example.org/StructureDefinition/ObservationishProfile" as CanonicalUrl, + }, + base: base.identifier, + fields: {}, + }; + + const index = mkTypeSchemaIndex([base, profile], {}); + const snap = index.collectSnapshotProfiles().find((s) => s.identifier.name === "ObservationishProfile"); + if (!snap) throw new Error("snapshot not built"); + + // recorded is inherited; value[x] is a choice declaration → skipped. + expect(snap.inheritedRequiredFields).toEqual(["recorded"]); + expect(snap.inheritedRequiredFields).not.toContain("value[x]"); + }); }); });