Repository: FHIR/sushi (ts)

Issue:
SUSHI should warn when multiple value[x] choices are used
SUSHI should log a warning if multiple value[x] choice elements are applied to an Instance. For example, both `valueString` and `valueCodeableConcept` cannot both be assigned on an Observation instance. In this case, the FHIR validator reports an error, but SUSHI does not.

Example FSH: https://fshschool.org/FSHOnline/#/share/3pKpG5X
Zulip thread: https://chat.fhir.org/#narrow/stream/215610-shorthand/topic/Multiple.20Observation.20values

Pull request description:
Check exported resources for presence of multiple choice values
**Description:**
When a choice element is defined in FHIR, it allows for values of different types with names based on those types. However, any choice element that is present on resource should have only one named value. For example, Observation.value[x] allows for many types, but an Observation resource should not have both valueString and valueInteger. After exporting a resource, check the resource for the presence of multiple values for the same choice element, and log an error when they are detected.

As assigned values are validated in exporters, the relevant StructureDefinitions will have their elements unfolded. Keep the same instance of that StructureDefinition that was used for validation so that the unfolded elements are present when performing the multiple choice validation check.

**Testing Instructions:**
Confirm that all tests pass. Confirm that tests provide sufficient coverage for the scenario described. I put most of the tests in `InstanceExporter.test.ts` since those were the first ones I wrote, but since all exporters use the same functions, I felt that it was not necessary to have as many tests in the other exporters.

**Related Issue:**
Fixes #1137.

Patch:
diff --git a/src/export/CodeSystemExporter.ts b/src/export/CodeSystemExporter.ts
index bf2b325..3409308 100644
--- a/src/export/CodeSystemExporter.ts
+++ b/src/export/CodeSystemExporter.ts
@@ -1,5 +1,5 @@
 import { FSHTank } from '../import/FSHTank';
-import { CodeSystem, CodeSystemConcept, PathPart } from '../fhirtypes';
+import { CodeSystem, CodeSystemConcept, PathPart, StructureDefinition } from '../fhirtypes';
 import {
   setPropertyOnDefinitionInstance,
   applyInsertRules,
@@ -9,7 +9,8 @@ import {
   validateInstanceFromRawValue,
   isExtension,
   replaceReferences,
-  splitOnPathPeriods
+  splitOnPathPeriods,
+  checkForMultipleChoice
 } from '../fhirtypes/common';
 import { FshCodeSystem } from '../fshtypes';
 import { CaretValueRule, ConceptRule } from '../fshtypes/rules';
@@ -104,7 +105,11 @@ export class CodeSystemExporter {
     }
   }
 
-  private setCaretPathRules(codeSystem: CodeSystem, rules: CaretValueRule[]) {
+  private setCaretPathRules(
+    codeSystem: CodeSystem,
+    rules: CaretValueRule[],
+    codeSystemSD: StructureDefinition
+  ) {
     // soft index resolution relies on the rule's path attribute.
     // a CaretValueRule is created with an empty path, so first
     // transform its arrayPath into a path.
@@ -130,7 +135,6 @@ export class CodeSystemExporter {
     // a codesystem is a specific case where the only implied values are going to be extension urls.
     // so, we only need to track rules that involve an extension.
     const ruleMap: Map<string, { pathParts: PathPart[] }> = new Map();
-    const codeSystemSD = codeSystem.getOwnStructureDefinition(this.fisher);
     // in order to validate rules that set values on contained resources, we need to track information from rules
     // that define the types of those resources. those types could be defined by rules on the "resourceType" element,
     // or they could be defined by the existing resource that is being assigned.
@@ -344,6 +348,7 @@ export class CodeSystemExporter {
       return;
     }
     const codeSystem = new CodeSystem();
+    const codeSystemSD = codeSystem.getOwnStructureDefinition(this.fisher);
     this.setMetadata(codeSystem, fshDefinition);
     this.setConcepts(
       codeSystem,
@@ -351,7 +356,8 @@ export class CodeSystemExporter {
     );
     this.setCaretPathRules(
       codeSystem,
-      fshDefinition.rules.filter(rule => rule instanceof CaretValueRule) as CaretValueRule[]
+      fshDefinition.rules.filter(rule => rule instanceof CaretValueRule) as CaretValueRule[],
+      codeSystemSD
     );
 
     // check for another code system with the same id
@@ -364,6 +370,7 @@ export class CodeSystemExporter {
     }
 
     cleanResource(codeSystem, (prop: string) => ['_sliceName', '_primitive'].includes(prop));
+    checkForMultipleChoice(fshDefinition, codeSystem, codeSystemSD);
     this.updateCount(codeSystem, fshDefinition);
     this.pkg.codeSystems.push(codeSystem);
     this.pkg.fshMap.set(codeSystem.getFileName(), {
diff --git a/src/export/InstanceExporter.ts b/src/export/InstanceExporter.ts
index 108c042..076a10d 100644
--- a/src/export/InstanceExporter.ts
+++ b/src/export/InstanceExporter.ts
@@ -23,7 +23,8 @@ import {
   createUsefulSlices,
   determineKnownSlices,
   setImpliedPropertiesOnInstance,
-  getMatchingContainedReferenceId
+  getMatchingContainedReferenceId,
+  checkForMultipleChoice
 } from '../fhirtypes/common';
 import { InstanceOfNotDefinedError } from '../errors/InstanceOfNotDefinedError';
 import { AbstractInstanceOfError } from '../errors/AbstractInstanceOfError';
@@ -922,6 +923,7 @@ export class InstanceExporter implements Fishable {
     );
     this.checkForNamelessSlices(fshDefinition, instanceDef, instanceOfStructureDefinition);
     cleanResource(instanceDef);
+    checkForMultipleChoice(fshDefinition, instanceDef, instanceOfStructureDefinition);
     this.pkg.instances.push(instanceDef);
     if (fshDefinition.usage !== 'Inline') {
       this.pkg.fshMap.set(instanceDef.getFileName(), {
diff --git a/src/export/StructureDefinitionExporter.ts b/src/export/StructureDefinitionExporter.ts
index 7f34dbe..e585cdf 100644
--- a/src/export/StructureDefinitionExporter.ts
+++ b/src/export/StructureDefinitionExporter.ts
@@ -67,7 +67,8 @@ import {
   getAllConcepts,
   TYPE_CHARACTERISTICS_CODE,
   TYPE_CHARACTERISTICS_EXTENSION,
-  LOGICAL_TARGET_EXTENSION
+  LOGICAL_TARGET_EXTENSION,
+  checkForMultipleChoice
 } from '../fhirtypes/common';
 import { Package } from './Package';
 import { isUri } from 'valid-url';
@@ -1548,6 +1549,12 @@ export class StructureDefinitionExporter implements Fishable {
       logger.log(err.severity, err.message, fshDefinition.sourceInfo);
     });
 
+    checkForMultipleChoice(
+      fshDefinition,
+      structDef,
+      structDef.getOwnStructureDefinition(this.fisher)
+    );
+
     // check for another structure definition with the same id
     // see https://www.hl7.org/fhir/resource.html#id
     // the structure definition has already been added to the package, so it's fine if it matches itself
diff --git a/src/export/ValueSetExporter.ts b/src/export/ValueSetExporter.ts
index 9a6019e..2ec0b1c 100644
--- a/src/export/ValueSetExporter.ts
+++ b/src/export/ValueSetExporter.ts
@@ -2,7 +2,8 @@ import {
   ValueSet,
   ValueSetComposeIncludeOrExclude,
   ValueSetComposeConcept,
-  PathPart
+  PathPart,
+  StructureDefinition
 } from '../fhirtypes';
 import { FSHTank } from '../import/FSHTank';
 import { FshValueSet, FshCode, ValueSetFilterValue, FshCodeSystem, Instance } from '../fshtypes';
@@ -24,7 +25,8 @@ import {
   validateInstanceFromRawValue,
   determineKnownSlices,
   setImpliedPropertiesOnInstance,
-  splitOnPathPeriods
+  splitOnPathPeriods,
+  checkForMultipleChoice
 } from '../fhirtypes/common';
 import { isUri } from 'valid-url';
 import { flatMap, partition, xor } from 'lodash';
@@ -269,11 +271,14 @@ export class ValueSetExporter {
     }
   }
 
-  private setCaretRules(valueSet: ValueSet, rules: CaretValueRule[]) {
+  private setCaretRules(
+    valueSet: ValueSet,
+    rules: CaretValueRule[],
+    valueSetSD: StructureDefinition
+  ) {
     resolveSoftIndexing(rules);
 
     const ruleMap: Map<string, { pathParts: PathPart[] }> = new Map();
-    const valueSetSD = valueSet.getOwnStructureDefinition(this.fisher);
     // in order to validate rules that set values on contained resources, we need to track information from rules
     // that define the types of those resources. those types could be defined by rules on the "resourceType" element,
     // or they could be defined by the existing resource that is being assigned.
@@ -416,10 +421,13 @@ export class ValueSetExporter {
     }
   }
 
-  private setConceptCaretRules(vs: ValueSet, rules: CaretValueRule[]) {
+  private setConceptCaretRules(
+    vs: ValueSet,
+    rules: CaretValueRule[],
+    valueSetSD: StructureDefinition
+  ) {
     resolveSoftIndexing(rules);
     const ruleMap: Map<string, { pathParts: PathPart[]; rule: CaretValueRule }> = new Map();
-    const valueSetSD = vs.getOwnStructureDefinition(this.fisher);
     for (const rule of rules) {
       const splitConcept = rule.pathArray[0].split('#');
       const system = splitConcept[0];
@@ -552,6 +560,7 @@ export class ValueSetExporter {
       return;
     }
     const vs = new ValueSet();
+    const valueSetSD = vs.getOwnStructureDefinition(this.fisher);
     this.setMetadata(vs, fshDefinition);
     const [conceptCaretRules, otherCaretRules] = partition(
       fshDefinition.rules.filter(rule => rule instanceof CaretValueRule) as CaretValueRule[],
@@ -559,7 +568,7 @@ export class ValueSetExporter {
         return caretRule.pathArray.length > 0;
       }
     );
-    this.setCaretRules(vs, otherCaretRules);
+    this.setCaretRules(vs, otherCaretRules, valueSetSD);
     this.setCompose(
       vs,
       fshDefinition.rules.filter(
@@ -567,7 +576,7 @@ export class ValueSetExporter {
       ) as ValueSetComponentRule[]
     );
     conceptCaretRules.forEach(rule => (rule.isCodeCaretRule = true));
-    this.setConceptCaretRules(vs, conceptCaretRules);
+    this.setConceptCaretRules(vs, conceptCaretRules, valueSetSD);
     if (vs.compose && vs.compose.include.length == 0) {
       throw new ValueSetComposeError(fshDefinition.name);
     }
@@ -582,6 +591,7 @@ export class ValueSetExporter {
     }
 
     cleanResource(vs, (prop: string) => ['_sliceName', '_primitive'].includes(prop));
+    checkForMultipleChoice(fshDefinition, vs, valueSetSD);
     this.pkg.valueSets.push(vs);
     this.pkg.fshMap.set(vs.getFileName(), {
       ...fshDefinition.sourceInfo,
diff --git a/src/fhirtypes/common.ts b/src/fhirtypes/common.ts
index 42f359b..45119bd 100644
--- a/src/fhirtypes/common.ts
+++ b/src/fhirtypes/common.ts
@@ -1647,3 +1647,65 @@ export function getMatchingContainedReferenceId(
     }
   }
 }
+
+export function checkForMultipleChoice(
+  fshDef: Profile | Extension | Logical | Resource | FshCodeSystem | FshValueSet | Instance,
+  fhirDef: { [key: string]: any },
+  structDef: StructureDefinition
+) {
+  checkChildrenForMultipleChoice(fshDef, fhirDef, structDef.elements[0]);
+}
+
+function checkChildrenForMultipleChoice(
+  fshDef: Profile | Extension | Logical | Resource | FshCodeSystem | FshValueSet | Instance,
+  instance: { [key: string]: any },
+  element: ElementDefinition
+) {
+  const children = element.children(true);
+  children.forEach(child => {
+    // does this child represent a choice element, such as value[x]?
+    // if so, check for choices
+    if (child.id.endsWith('[x]')) {
+      // get the element names for each type choice
+      const idStart = splitOnPathPeriods(child.id).slice(-1)[0].slice(0, -3);
+      const availableChoices = child.type.map(edType => `${idStart}${upperFirst(edType.code)}`);
+      if (availableChoices.length > 1) {
+        const existingChoices = availableChoices.filter(choice => {
+          return instance[choice] != null || instance[`_${choice}`] != null;
+        });
+        if (existingChoices.length > 1) {
+          logger.error(
+            `${fshDef.name} contains multiple choice value assignments for choice element ${child.id}.`,
+            fshDef.sourceInfo
+          );
+        }
+      }
+    }
+    // does the instance have an object value for this element?
+    // if so, recursively check that object.
+    // since there may also be children of primitives, also check underscore properties
+    const childPathEnd = child.path.split('.').slice(-1)[0];
+    if (instance[childPathEnd] != null && typeof instance[childPathEnd] === 'object') {
+      if (Array.isArray(instance[childPathEnd])) {
+        instance[childPathEnd].forEach((childProperty: any) => {
+          if (childProperty != null && typeof childProperty === 'object') {
+            checkChildrenForMultipleChoice(fshDef, childProperty, child);
+          }
+        });
+      } else {
+        checkChildrenForMultipleChoice(fshDef, instance[childPathEnd], child);
+      }
+    }
+    if (instance[`_${childPathEnd}`] != null && typeof instance[`_${childPathEnd}`] === 'object') {
+      if (Array.isArray(instance[`_${childPathEnd}`])) {
+        instance[`_${childPathEnd}`].forEach((childProperty: any) => {
+          if (childProperty != null && typeof childProperty === 'object') {
+            checkChildrenForMultipleChoice(fshDef, childProperty, child);
+          }
+        });
+      } else {
+        checkChildrenForMultipleChoice(fshDef, instance[`_${childPathEnd}`], child);
+      }
+    }
+  });
+}


Test patch:
diff --git a/test/export/CodeSystemExporter.test.ts b/test/export/CodeSystemExporter.test.ts
index 99fd9f5..3a9b0aa 100644
--- a/test/export/CodeSystemExporter.test.ts
+++ b/test/export/CodeSystemExporter.test.ts
@@ -1211,6 +1211,36 @@ describe('CodeSystemExporter', () => {
     });
   });
 
+  it('should output an error when a choice element has values assigned to more than one choice type', () => {
+    const codeSystem = new FshCodeSystem('MultiChoiceSystem')
+      .withFile('MultipleChoice.fsh')
+      .withLocation([3, 4, 8, 24]);
+    const extensionUrl = new CaretValueRule('');
+    extensionUrl.caretPath = 'extension[0].url';
+    extensionUrl.value = 'http://example.org/SomeExt';
+    const extensionString = new CaretValueRule('');
+    extensionString.caretPath = 'extension[0].valueString';
+    extensionString.value = 'multi value';
+    const extensionInteger = new CaretValueRule('');
+    extensionInteger.caretPath = 'extension[0].valueInteger';
+    extensionInteger.value = BigInt(24);
+    const conceptRule = new ConceptRule('bar', 'Bar', 'Bar');
+    codeSystem.rules.push(extensionUrl, extensionString, extensionInteger, conceptRule);
+    doc.codeSystems.set(codeSystem.name, codeSystem);
+
+    const exported = exporter.export().codeSystems;
+    expect(exported.length).toBe(1);
+    expect(exported[0].extension[0]).toEqual({
+      url: 'http://example.org/SomeExt',
+      valueString: 'multi value',
+      valueInteger: 24
+    });
+    expect(loggerSpy.getAllMessages('error')).toHaveLength(1);
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /MultiChoiceSystem contains multiple choice value assignments for choice element CodeSystem\.extension\.value\[x\]\..*File: MultipleChoice\.fsh.*Line: 3 - 8\D*/s
+    );
+  });
+
   it('should not override count when ^count is provided by user', () => {
     const codeSystem = new FshCodeSystem('MyCodeSystem');
     const rule = new CaretValueRule('');
diff --git a/test/export/InstanceExporter.test.ts b/test/export/InstanceExporter.test.ts
index 75de121..ff3a19e 100644
--- a/test/export/InstanceExporter.test.ts
+++ b/test/export/InstanceExporter.test.ts
@@ -5330,6 +5330,155 @@ describe('InstanceExporter', () => {
       expect(loggerSpy.getAllMessages('warn')).toHaveLength(0);
     });
 
+    it('should output an error when a choice element has values assigned to more than one choice type', () => {
+      // * multipleBirthBoolean = true
+      // * multipleBirthInteger = 2
+      const multipleBirthBoolean = new AssignmentRule('multipleBirthBoolean');
+      multipleBirthBoolean.value = true;
+      const multipleBirthInteger = new AssignmentRule('multipleBirthInteger')
+        .withFile('Twins.fsh')
+        .withLocation([4, 3, 4, 34]);
+      multipleBirthInteger.value = BigInt(2);
+      patientInstance.rules.push(multipleBirthBoolean, multipleBirthInteger);
+
+      const exported = exportInstance(patientInstance);
+      expect(exported.multipleBirthBoolean).toBe(true);
+      expect(exported.multipleBirthInteger).toBe(2);
+      expect(loggerSpy.getAllMessages('error')).toHaveLength(1);
+      expect(loggerSpy.getLastMessage('error')).toMatch(
+        /Bar contains multiple choice value assignments for choice element Patient\.multipleBirth\[x\]\..*File: PatientInstance\.fsh.*Line: 10 - 20\D*/s
+      );
+      expect(loggerSpy.getAllMessages('warn')).toHaveLength(0);
+    });
+
+    it('should output an error when a choice element has values assigned to more than one choice type, some of which are a complex type', () => {
+      // Instance: sample-observation
+      // InstanceOf: Observation
+      // * status = #draft
+      // * code = #123
+      // * valueString = "my string value"
+      // * valueCodeableConcept.text = "explanation of codeable concept"
+      const obsInstance = new Instance('sample-observation')
+        .withFile('Observations.fsh')
+        .withLocation([8, 3, 15, 44]);
+      obsInstance.instanceOf = 'Observation';
+      const obsStatus = new AssignmentRule('status');
+      obsStatus.value = new FshCode('draft');
+      const obsCode = new AssignmentRule('code');
+      obsCode.value = new FshCode('123');
+      const obsString = new AssignmentRule('valueString');
+      obsString.value = 'my string value';
+      const obsCodeableConceptText = new AssignmentRule('valueCodeableConcept.text');
+      obsCodeableConceptText.value = 'explanation of codeable concept';
+      obsInstance.rules.push(obsStatus, obsCode, obsString, obsCodeableConceptText);
+
+      const exported = exportInstance(obsInstance);
+      expect(exported.valueString).toBe('my string value');
+      expect(exported.valueCodeableConcept.text).toBe('explanation of codeable concept');
+      expect(loggerSpy.getAllMessages('error')).toHaveLength(1);
+      expect(loggerSpy.getLastMessage('error')).toMatch(
+        /sample-observation contains multiple choice value assignments for choice element Observation\.value\[x\]\..*File: Observations\.fsh.*Line: 8 - 15\D*/s
+      );
+      expect(loggerSpy.getAllMessages('warn')).toHaveLength(0);
+    });
+
+    it('should not output an error when a multiple-cardinality choice element has different types at different indices', () => {
+      // Instance: sample-observation
+      // InstanceOf: Observation
+      // * status = #draft
+      // * code = #123
+      // * component[0].code = #123string
+      // * component[0].valueString = "my string value"
+      // * component[1].code = #123codeableConcept
+      // * component[1].valueCodeableConcept = http://example.org#paper "the paper"
+      const obsInstance = new Instance('sample-observation');
+      obsInstance.instanceOf = 'Observation';
+      const obsStatus = new AssignmentRule('status');
+      obsStatus.value = new FshCode('draft');
+      const obsCode = new AssignmentRule('code');
+      obsCode.value = new FshCode('123');
+      const firstComponentCode = new AssignmentRule('component[0].code');
+      firstComponentCode.value = new FshCode('123string');
+      const firstComponentValue = new AssignmentRule('component[0].valueString');
+      firstComponentValue.value = 'my string value';
+      const secondComponentCode = new AssignmentRule('component[1].code');
+      secondComponentCode.value = new FshCode('123codeableConcept');
+      const secondComponentValue = new AssignmentRule('component[1].valueCodeableConcept');
+      secondComponentValue.value = new FshCode('paper', 'http://example.org', 'the paper');
+
+      obsInstance.rules.push(
+        obsStatus,
+        obsCode,
+        firstComponentCode,
+        firstComponentValue,
+        secondComponentCode,
+        secondComponentValue
+      );
+      const exported = exportInstance(obsInstance);
+      expect(exported.component[0].valueString).toBe('my string value');
+      expect(exported.component[1].valueCodeableConcept).toEqual({
+        coding: [
+          {
+            code: 'paper',
+            system: 'http://example.org',
+            display: 'the paper'
+          }
+        ]
+      });
+      expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
+    });
+
+    it('should output an error when a choice element within another element has values assigned to more than one choice type', () => {
+      // * extension[0].url = "https://example.org/SomeExt"
+      // * extension[0].valueString = "extension value is false"
+      // * extension[0].valueBoolean = false
+      const extensionUrl = new AssignmentRule('extension[0].url');
+      extensionUrl.value = 'https://example.org/SomeExt';
+      const extensionString = new AssignmentRule('extension[0].valueString');
+      extensionString.value = 'extension value is false';
+      const extensionBoolean = new AssignmentRule('extension[0].valueBoolean');
+      extensionBoolean.value = false;
+      extensionBoolean.rawValue = 'false';
+      patientInstance.rules.push(extensionUrl, extensionString, extensionBoolean);
+
+      const exported = exportInstance(patientInstance);
+      expect(exported.extension[0]).toEqual({
+        url: 'https://example.org/SomeExt',
+        valueString: 'extension value is false',
+        valueBoolean: false
+      });
+      expect(loggerSpy.getAllMessages('error')).toHaveLength(1);
+      expect(loggerSpy.getLastMessage('error')).toMatch(
+        /Bar contains multiple choice value assignments for choice element Patient\.extension\.value\[x\]\..*File: PatientInstance\.fsh.*Line: 10 - 20\D*/s
+      );
+      expect(loggerSpy.getAllMessages('warn')).toHaveLength(0);
+    });
+
+    it('should output an error when a choice element that is a descendant of a primitive has values assigned to more than one type', () => {
+      // * gender.extension[0].url = "https://example.org/SomeExt"
+      // * gender.extension[0].valueString = "patient's gender is unknowable"
+      // * gender.extension[0].valueCode = #unknowable
+      const extensionUrl = new AssignmentRule('gender.extension[0].url');
+      extensionUrl.value = 'https://example.org/SomeExt';
+      const extensionString = new AssignmentRule('gender.extension[0].valueString');
+      extensionString.value = "patient's gender is unknowable";
+      const extensionCode = new AssignmentRule('gender.extension[0].valueCode');
+      extensionCode.value = new FshCode('unknowable');
+      patientInstance.rules.push(extensionUrl, extensionString, extensionCode);
+
+      const exported = exportInstance(patientInstance);
+      expect(exported._gender.extension[0]).toEqual({
+        url: 'https://example.org/SomeExt',
+        valueString: "patient's gender is unknowable",
+        valueCode: 'unknowable'
+      });
+      expect(loggerSpy.getAllMessages('error')).toHaveLength(1);
+      expect(loggerSpy.getLastMessage('error')).toMatch(
+        /Bar contains multiple choice value assignments for choice element Patient\.gender\.extension\.value\[x\]\..*File: PatientInstance\.fsh.*Line: 10 - 20\D*/s
+      );
+      expect(loggerSpy.getAllMessages('warn')).toHaveLength(0);
+    });
+
     it('should assign cardinality 1..n elements that are assigned by array pattern[x] from a parent on the SD', () => {
       const assignedValRule = new AssignmentRule('maritalStatus');
       assignedValRule.value = new FshCode('foo', 'http://foo.com');
diff --git a/test/export/StructureDefinitionExporter.test.ts b/test/export/StructureDefinitionExporter.test.ts
index bf35721..797b0f5 100644
--- a/test/export/StructureDefinitionExporter.test.ts
+++ b/test/export/StructureDefinitionExporter.test.ts
@@ -5648,6 +5648,36 @@ describe('StructureDefinitionExporter R4', () => {
       );
     });
 
+    it('should apply AssignmentRules to different types of a choice element', () => {
+      // Profile: MyObservation
+      // Parent: Observation
+      // * valueString = "Hello"
+      // * valueCodeableConcept = http://example.org#world
+      const profile = new Profile('MyObservation');
+      profile.parent = 'Observation';
+      const stringRule = new AssignmentRule('valueString');
+      stringRule.value = 'Hello';
+      const codeableConceptRule = new AssignmentRule('valueCodeableConcept');
+      codeableConceptRule.value = new FshCode('world', 'http://example.org');
+      profile.rules.push(stringRule, codeableConceptRule);
+      exporter.exportStructDef(profile);
+      const sd = pkg.profiles[0];
+
+      const stringChoice = sd.findElement('Observation.value[x]:valueString');
+      expect(stringChoice.patternString).toBe('Hello');
+      const codeableConceptChoice = sd.findElement('Observation.value[x]:valueCodeableConcept');
+      expect(codeableConceptChoice.patternCodeableConcept).toEqual({
+        coding: [
+          {
+            code: 'world',
+            system: 'http://example.org'
+          }
+        ]
+      });
+
+      expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
+    });
+
     it('should apply a Code AssignmentRule and replace the local complete code system name with its url', () => {
       const profile = new Profile('LightObservation');
       profile.parent = 'Observation';
@@ -8633,6 +8663,42 @@ describe('StructureDefinitionExporter R4', () => {
         null
       ]);
     });
+
+    it('should output an error when a choice element has values assigned to more than one choice type', () => {
+      // Profile: MyObservation
+      // Parent: Observation
+      // * ^extension[0].url = "http://example.org/SomeExt"
+      // * ^extension[0].valueString = "string value"
+      // * ^extension[0].valueInteger = 7
+      const profile = new Profile('MyObservation')
+        .withFile('Observation.fsh')
+        .withLocation([8, 3, 15, 25]);
+      profile.parent = 'Observation';
+      const extensionUrl = new CaretValueRule('');
+      extensionUrl.caretPath = 'extension[0].url';
+      extensionUrl.value = 'http://example.org/SomeExt';
+      const extensionString = new CaretValueRule('');
+      extensionString.caretPath = 'extension[0].valueString';
+      extensionString.value = 'string value';
+      const extensionInteger = new CaretValueRule('');
+      extensionInteger.caretPath = 'extension[0].valueInteger';
+      extensionInteger.value = BigInt(7);
+      profile.rules.push(extensionUrl, extensionString, extensionInteger);
+      doc.profiles.set(profile.name, profile);
+
+      exporter.exportStructDef(profile);
+      const sd = pkg.profiles[0];
+      expect(sd.extension).toHaveLength(1);
+      expect(sd.extension[0]).toEqual({
+        url: 'http://example.org/SomeExt',
+        valueString: 'string value',
+        valueInteger: 7
+      });
+      expect(loggerSpy.getAllMessages('error')).toHaveLength(1);
+      expect(loggerSpy.getLastMessage('error')).toMatch(
+        /MyObservation contains multiple choice value assignments for choice element StructureDefinition\.extension\.value\[x\]\..*File: Observation\.fsh.*Line: 8 - 15\D*/s
+      );
+    });
   });
 
   describe('#ObeysRule', () => {
diff --git a/test/export/ValueSetExporter.test.ts b/test/export/ValueSetExporter.test.ts
index ffb34b7..248a25e 100644
--- a/test/export/ValueSetExporter.test.ts
+++ b/test/export/ValueSetExporter.test.ts
@@ -3195,6 +3195,36 @@ describe('ValueSetExporter', () => {
     expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
   });
 
+  it('should output an error when a choice element has values assigned to more than one choice type', () => {
+    const valueSet = new FshValueSet('BreakfastVS')
+      .withFile('Breakfast.fsh')
+      .withLocation([8, 3, 25, 33]);
+    valueSet.title = 'Breakfast Values';
+    const extensionUrl = new CaretValueRule('');
+    extensionUrl.caretPath = 'extension[0].url';
+    extensionUrl.value = 'http://example.org/SomeExt';
+    const extensionString = new CaretValueRule('');
+    extensionString.caretPath = 'extension[0].valueString';
+    extensionString.value = 'string value';
+    const extensionInteger = new CaretValueRule('');
+    extensionInteger.caretPath = 'extension[0].valueInteger';
+    extensionInteger.value = BigInt(7);
+    valueSet.rules.push(extensionUrl, extensionString, extensionInteger);
+    doc.valueSets.set(valueSet.name, valueSet);
+
+    const exported = exporter.export().valueSets;
+    expect(exported.length).toBe(1);
+    expect(exported[0].extension[0]).toEqual({
+      url: 'http://example.org/SomeExt',
+      valueString: 'string value',
+      valueInteger: 7
+    });
+    expect(loggerSpy.getAllMessages('error')).toHaveLength(1);
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /BreakfastVS contains multiple choice value assignments for choice element ValueSet\.extension\.value\[x\]\..*File: Breakfast\.fsh.*Line: 8 - 25\D*/s
+    );
+  });
+
   describe('#insertRules', () => {
     let vs: FshValueSet;
     let ruleSet: RuleSet;