Repository: FHIR/sushi
Pull request #870 (2021-07-08 17:18:41)

Add support for type checking when fixing Canonical types
This addresses [CIMPL-783](https://standardhealthrecord.atlassian.net/browse/CIMPL-783) by adding support for type checking when fixing `Canonical` values. The type checking checks the type of the SD pointed to by the `Canonical` keyword against the `targetProfile` on the element being assigned to. If the type of the SD pointed to is not a child of one of the options listed in the `targetProfile`, then an error is logged. Otherwise, no error is logged.

If the type of the SD pointed to cannot be determined, no type checking is done, and the assignment is just allowed. Generally this is set up to mimic how things work for `Reference`.

cmoesel: @mint-thompson and @ngfreiter -- I think Mint's example above likely works because there is no such thing as a canonical URL for an `Observation` (which is also a slight flaw in Nick's test case). Canonical URLs identify definitional resources in FHIR -- so they really only apply to a subset of resource types; namely those types that have a top-level `url` property (which _is_ the canonical URL).  So, to really test this, you should be using one of those resources.

So, I think the test case should be updated to be more realistic in that way -- and I agree w/ Mint that it would be good to have a positive test case as well.
ngfreiter: I think there are more than flaws in the test case here, but also some flaws in the implementation that I've got to fix. Because I do think @mint-thompson's case should be an error.
ngfreiter: Ok I think https://github.com/FHIR/sushi/pull/870/commits/decc7618e57721feaf7ac4c81ac922c818890b08 should be better. To better explain what the issue was, my original implementation was checking the `sdType` of the thing being referenced, which doesn't make a lot of sense. For example in this case:
```
Instance: CIn
InstanceOf: CarePlan
* instantiatesCanonical = Canonical(P)

Profile: P
Parent: PlanDefinition
```
the first implementation would check that the `sdType` of `P` is `PlanDefinition`, and allow this. But this is wrong, because `P` itself is a `StructureDefinition`, which is not one of the allowed types of `instantiatesCanonical`. And then if the thing pointed to by the `Canonical` was an `Instance`, my implementation would find no `sdType` at all, and just allow everything through, which is again wrong.

The new approach is to check the `resourceType` of the thing pointed to by the `Canonical`. So if the thing pointed to is a `Profile`, the type to check is `StructureDefinition` itself, not the type of the `StructureDefinition`. The one tricky thing here, is that on an `Instance` that is not yet exported, we don't know the `resourceType`, only the `InstanceOf`, but since we check if any ancestors conform to the expected types, I think using the `InstanceOf` value leads to checking against the `resourceType`, since that will be an ancestor of the `InstanceOf`. Sorry if I'm explaining that poorly, let me know if it isn't making sense.
ngfreiter: Oh and I added more tests per @mint-thompson's requests (and tried to make those tests more relevant, per @cmoesel's request).

Patch:
diff --git a/src/errors/InvalidTypeError.ts b/src/errors/InvalidTypeError.ts
index 8a07ef5..ecd4828 100644
--- a/src/errors/InvalidTypeError.ts
+++ b/src/errors/InvalidTypeError.ts
@@ -31,6 +31,8 @@ function allowedTypesToString(types: ElementDefinitionType[]): string {
   types.forEach(t => {
     if (isReferenceType(t.code)) {
       strings.push(`Reference(${(t.targetProfile ?? []).join(' | ')})`);
+    } else if (t.code === 'canonical') {
+      strings.push(`Canonical(${(t.targetProfile ?? []).join(' | ')})`);
     } else if (t.profile?.length > 0) {
       strings.push(...t.profile);
     } else {
diff --git a/src/export/Package.ts b/src/export/Package.ts
index 8552a5f..f874af6 100644
--- a/src/export/Package.ts
+++ b/src/export/Package.ts
@@ -76,7 +76,8 @@ export class Package implements Fishable {
         name: result instanceof InstanceDefinition ? result._instanceMeta.name : result.name,
         instanceUsage:
           result instanceof InstanceDefinition ? result._instanceMeta.usage : undefined,
-        url: result.url
+        url: result.url,
+        resourceType: result.resourceType
       };
       if (result instanceof StructureDefinition) {
         metadata.sdType = result.type;
diff --git a/src/fhirdefs/FHIRDefinitions.ts b/src/fhirdefs/FHIRDefinitions.ts
index 16966c3..5d9439a 100644
--- a/src/fhirdefs/FHIRDefinitions.ts
+++ b/src/fhirdefs/FHIRDefinitions.ts
@@ -166,7 +166,7 @@ export class FHIRDefinitions implements Fishable {
     }
   }
 
-  fishForPredefinedResourceMetadata(item: string, ...types: Type[]): any | undefined {
+  fishForPredefinedResourceMetadata(item: string, ...types: Type[]): Metadata | undefined {
     const resource = this.fishForPredefinedResource(item, ...types);
     if (resource) {
       return {
@@ -175,7 +175,8 @@ export class FHIRDefinitions implements Fishable {
         sdType: resource.type as string,
         url: resource.url as string,
         parent: resource.baseDefinition as string,
-        abstract: resource.abstract as boolean
+        abstract: resource.abstract as boolean,
+        resourceType: resource.resourceType as string
       };
     }
   }
@@ -241,7 +242,8 @@ export class FHIRDefinitions implements Fishable {
         sdType: result.type as string,
         url: result.url as string,
         parent: result.baseDefinition as string,
-        abstract: result.abstract as boolean
+        abstract: result.abstract as boolean,
+        resourceType: result.resourceType as string
       };
     }
   }
diff --git a/src/fhirtypes/ElementDefinition.ts b/src/fhirtypes/ElementDefinition.ts
index 6e8f20d..3644278 100644
--- a/src/fhirtypes/ElementDefinition.ts
+++ b/src/fhirtypes/ElementDefinition.ts
@@ -1446,23 +1446,11 @@ export class ElementDefinition {
           this.parent()?.type?.[0]?.code === 'CodeableReference'
             ? this.parent()
             : this;
-        // If no targetProfile is present, there is nothing to check the value against, so just assign it
-        if (value.sdType && referenceConstrainingElement.type[0].targetProfile) {
-          const validTypes: string[] = [];
-          referenceConstrainingElement.type[0].targetProfile.forEach(tp => {
-            const tpType = fisher.fishForMetadata(tp)?.sdType;
-            if (tpType) {
-              validTypes.push(tpType);
-            }
-          });
-
-          const referenceLineage = this.getTypeLineage(value.sdType, fisher);
-          if (!referenceLineage.some(md => validTypes.includes(md.sdType))) {
-            throw new InvalidTypeError(
-              `Reference(${value.sdType})`,
-              referenceConstrainingElement.type
-            );
-          }
+        if (!referenceConstrainingElement.typeSatisfiesTargetProfile(value.sdType, fisher)) {
+          throw new InvalidTypeError(
+            `Reference(${value.sdType})`,
+            referenceConstrainingElement.type
+          );
         }
         this.assignFHIRValue(value.toString(), value.toFHIRReference(), exactly, 'Reference');
         break;
@@ -1481,6 +1469,9 @@ export class ElementDefinition {
           Type.Instance
         );
         let canonicalUrl: string;
+        if (!this.typeSatisfiesTargetProfile(canonicalDefinition?.resourceType, fisher)) {
+          throw new InvalidTypeError(`Canonical(${canonicalDefinition.resourceType})`, this.type);
+        }
         if (canonicalDefinition?.url) {
           canonicalUrl = canonicalDefinition.url;
         } else if (canonicalDefinition?.id && canonicalDefinition.instanceUsage === 'Inline') {
@@ -1648,6 +1639,30 @@ export class ElementDefinition {
     }
   }
 
+  /**
+   * @param sdType - The type to check
+   * @param fisher - A fishable implementation for finding definitions and metadata
+   * @returns - False if the type does not satisfy the targetProfile, true otherwise (or if it can't be determined)
+   */
+  private typeSatisfiesTargetProfile(sdType: string, fisher: Fishable): boolean {
+    // If no targetProfile is present, there is nothing to check the value against, so just allow it
+    if (sdType && this.type[0].targetProfile) {
+      const validTypes: string[] = [];
+      this.type[0].targetProfile.forEach(tp => {
+        const tpType = fisher.fishForMetadata(tp)?.sdType;
+        if (tpType) {
+          validTypes.push(tpType);
+        }
+      });
+
+      const typeLineage = this.getTypeLineage(sdType, fisher);
+      if (!typeLineage.some(md => validTypes.includes(md.sdType))) {
+        return false;
+      }
+    }
+    return true;
+  }
+
   /**
    * Checks if an element is assigned by a fixed[x] or pattern[x] on its direct parent
    * @returns {any} the value the element is assigned to by its parent, undefined if value is not assigned
diff --git a/src/import/FSHTank.ts b/src/import/FSHTank.ts
index 65ba314..1374ee7 100644
--- a/src/import/FSHTank.ts
+++ b/src/import/FSHTank.ts
@@ -261,6 +261,7 @@ export class FSHTank implements Fishable {
       ) {
         meta.url = getUrlFromFshDefinition(result, this.config.canonical);
         meta.parent = result.parent;
+        meta.resourceType = 'StructureDefinition';
         if (result instanceof Logical) {
           // Logical models should always use an absolute URL as their StructureDefinition.type
           // unless HL7 published them. In that case, the URL is relative to
@@ -271,6 +272,11 @@ export class FSHTank implements Fishable {
         }
       } else if (result instanceof FshValueSet || result instanceof FshCodeSystem) {
         meta.url = getUrlFromFshDefinition(result, this.config.canonical);
+        if (result instanceof FshValueSet) {
+          meta.resourceType = 'ValueSet';
+        } else {
+          meta.resourceType = 'CodeSystem';
+        }
       } else if (result instanceof Instance) {
         result.rules?.forEach(r => {
           if (r.path === 'url' && r instanceof AssignmentRule && typeof r.value === 'string') {
diff --git a/src/utils/Fishable.ts b/src/utils/Fishable.ts
index 098d9e3..b3fc624 100644
--- a/src/utils/Fishable.ts
+++ b/src/utils/Fishable.ts
@@ -18,6 +18,7 @@ export interface Metadata {
   id: string;
   name: string;
   sdType?: string;
+  resourceType?: string;
   url?: string;
   parent?: string;
   abstract?: boolean;
diff --git a/src/utils/MasterFisher.ts b/src/utils/MasterFisher.ts
index cc20ff0..4a7034f 100644
--- a/src/utils/MasterFisher.ts
+++ b/src/utils/MasterFisher.ts
@@ -3,6 +3,7 @@ import { FSHTank } from '../import';
 import { FHIRDefinitions } from '../fhirdefs';
 import { Package } from '../export';
 import { logger } from './FSHLogger';
+import { Instance } from '../fshtypes';
 
 /**
  * The MasterFisher can fish from the tank, the FHIR definitions, and the package that is currently
@@ -70,6 +71,14 @@ export class MasterFisher implements Fishable {
         if (fishable instanceof FSHTank) {
           result.sdType = this.findSdType(result, types, fishables);
         }
+        // When an Instance comes from the FSHTank, the FSHTank doesn't know its resourceType,
+        // only its InstanceOf. But here we have access to the other fishers, so we can try
+        // to figure that resourceType out here
+        if (fishable instanceof FSHTank && !result.resourceType) {
+          result.resourceType = this.fishForMetadata(
+            (fishable.fish(item, Type.Instance) as Instance)?.instanceOf
+          )?.sdType;
+        }
         return result;
       }
     }


Test patch:
diff --git a/test/export/InstanceExporter.test.ts b/test/export/InstanceExporter.test.ts
index e9a0497..a979db8 100644
--- a/test/export/InstanceExporter.test.ts
+++ b/test/export/InstanceExporter.test.ts
@@ -139,6 +139,7 @@ describe('InstanceExporter', () => {
     let valueSetInstance: Instance;
     let respRateInstance: Instance;
     let bundleInstance: Instance;
+    let carePlanInstance: Instance;
     beforeEach(() => {
       questionnaire = new Profile('TestQuestionnaire');
       questionnaire.parent = 'Questionnaire';
@@ -163,6 +164,9 @@ describe('InstanceExporter', () => {
       patientProfInstance = new Instance('Baz');
       patientProfInstance.instanceOf = 'TestPatientProf';
       doc.instances.set(patientProfInstance.name, patientProfInstance);
+      carePlanInstance = new Instance('C');
+      carePlanInstance.instanceOf = 'CarePlan';
+      doc.instances.set(carePlanInstance.name, carePlanInstance);
       lipidInstance = new Instance('Bam')
         .withFile('LipidInstance.fsh')
         .withLocation([10, 1, 20, 30]);
@@ -1696,6 +1700,75 @@ describe('InstanceExporter', () => {
       );
     });
 
+    it('should assign a Canonical that is one of the valid types', () => {
+      const assignedRefRule = new AssignmentRule('instantiatesCanonical');
+      const pdInstance = new Instance('TestPD');
+      pdInstance.instanceOf = 'PlanDefinition';
+      const urlRule = new AssignmentRule('url');
+      urlRule.value = 'http://example.org/PlanDefition/1';
+      pdInstance.rules.push(urlRule);
+      doc.instances.set(pdInstance.name, pdInstance);
+      assignedRefRule.value = new FshCanonical('TestPD');
+      // * instantiatesCanonical = Canonical(TestPD)
+      carePlanInstance.rules.push(assignedRefRule);
+
+      const exported = exportInstance(carePlanInstance);
+      expect(exported.instantiatesCanonical).toEqual(['http://example.org/PlanDefition/1']); // instantiatesCanonical is set
+    });
+
+    it('should assign a Canonical that is a child of the valid types', () => {
+      const assignedRefRule = new AssignmentRule('instantiatesCanonical');
+      const pdProfile = new Profile('PlanDefProfile');
+      pdProfile.parent = 'PlanDefinition';
+      doc.profiles.set(pdProfile.name, pdProfile);
+      const pdInstance = new Instance('PlanDefInstance');
+      pdInstance.instanceOf = 'PlanDefProfile';
+      const urlRule = new AssignmentRule('url');
+      urlRule.value = 'http://example.org/PlanDefition/1';
+      pdInstance.rules.push(urlRule);
+      doc.instances.set(pdInstance.name, pdInstance);
+      assignedRefRule.value = new FshCanonical('PlanDefInstance');
+      // * instantiatesCanonical = Canonical(PlanDefInstance)
+      carePlanInstance.rules.push(assignedRefRule);
+
+      const exported = exportInstance(carePlanInstance);
+      expect(exported.instantiatesCanonical).toEqual(['http://example.org/PlanDefition/1']); // instantiatesCanonical is set
+    });
+
+    it('should log an error when an invalid canonical is assigned', () => {
+      const assignedRefRule = new AssignmentRule('instantiatesCanonical');
+      const vsInstance = new Instance('TestVS');
+      vsInstance.instanceOf = 'ValueSet';
+      doc.instances.set(vsInstance.name, vsInstance);
+      assignedRefRule.value = new FshCanonical('TestVS');
+      // * instantiatesCanonical = Canonical(TestVS)
+      carePlanInstance.rules.push(assignedRefRule);
+
+      const exported = exportInstance(carePlanInstance);
+      expect(exported.instantiatesCanonical).toEqual(undefined); // instantiatesCanonical is not set with invalid type
+      expect(loggerSpy.getFirstMessage('error')).toMatch(
+        /The type "Canonical\(ValueSet\)" does not match any of the allowed types\D*/s
+      );
+    });
+
+    it('should log an error when an already exported invalid canonical is assigned', () => {
+      const assignedRefRule = new AssignmentRule('instantiatesCanonical');
+      const vsInstance = new Instance('TestVS');
+      vsInstance.instanceOf = 'ValueSet';
+      doc.instances.set(vsInstance.name, vsInstance);
+      // First export the VS so that it is fished from  the package instead of FSHTank
+      exportInstance(vsInstance);
+      assignedRefRule.value = new FshCanonical('TestVS');
+      // * instantiatesCanonical = Canonical(TestVS)
+      carePlanInstance.rules.push(assignedRefRule);
+
+      const exported = exportInstance(carePlanInstance);
+      expect(exported.instantiatesCanonical).toEqual(undefined); // instantiatesCanonical is not set with invalid type
+      expect(loggerSpy.getMessageAtIndex(1, 'error')).toMatch(
+        /The type "Canonical\(ValueSet\)" does not match any of the allowed types\D*/s
+      );
+    });
+
     // Assigning codes from local systems
     it('should assign a code to a top level element while replacing the local code system name with its url', () => {
       const brightInstance = new Instance('BrightObservation');
diff --git a/test/export/Package.test.ts b/test/export/Package.test.ts
index 788d0d2..f203a9e 100644
--- a/test/export/Package.test.ts
+++ b/test/export/Package.test.ts
@@ -361,7 +361,8 @@ describe('Package', () => {
         name: 'Funny',
         sdType: 'Condition',
         url: 'http://hl7.org/fhir/us/minimal/StructureDefinition/fun-ny',
-        parent: 'http://hl7.org/fhir/StructureDefinition/Condition'
+        parent: 'http://hl7.org/fhir/StructureDefinition/Condition',
+        resourceType: 'StructureDefinition'
       });
       expect(pkg.fishForMetadata('Funny', Type.Profile)).toEqual(funnyProfile);
       expect(
@@ -379,7 +380,8 @@ describe('Package', () => {
         name: 'PoorTaste',
         sdType: 'Extension',
         url: 'http://hl7.org/fhir/us/minimal/StructureDefinition/poor-taste',
-        parent: 'http://hl7.org/fhir/StructureDefinition/Extension'
+        parent: 'http://hl7.org/fhir/StructureDefinition/Extension',
+        resourceType: 'StructureDefinition'
       });
       expect(pkg.fishForMetadata('PoorTaste', Type.Extension)).toEqual(poorTasteExtensionByID);
       expect(
@@ -397,7 +399,8 @@ describe('Package', () => {
         name: 'WheatBeer',
         sdType: 'wheat-beer',
         url: 'http://hl7.org/fhir/us/minimal/StructureDefinition/wheat-beer',
-        parent: 'http://hl7.org/fhir/StructureDefinition/Element'
+        parent: 'http://hl7.org/fhir/StructureDefinition/Element',
+        resourceType: 'StructureDefinition'
       });
       expect(pkg.fishForMetadata('WheatBeer', Type.Logical)).toEqual(wheatBeerLogicalByID);
       expect(
@@ -415,7 +418,8 @@ describe('Package', () => {
         name: 'Destination',
         sdType: 'Destination',
         url: 'http://hl7.org/fhir/us/minimal/StructureDefinition/Destination',
-        parent: 'http://hl7.org/fhir/StructureDefinition/DomainResource'
+        parent: 'http://hl7.org/fhir/StructureDefinition/DomainResource',
+        resourceType: 'StructureDefinition'
       });
       expect(pkg.fishForMetadata('Destination', Type.Resource)).toEqual(destinationResourceByID);
       expect(
@@ -431,7 +435,8 @@ describe('Package', () => {
       expect(soupsValueSetByID).toEqual({
         id: 'soup-flavors',
         name: 'Soups',
-        url: 'http://hl7.org/fhir/us/minimal/ValueSet/soup-flavors'
+        url: 'http://hl7.org/fhir/us/minimal/ValueSet/soup-flavors',
+        resourceType: 'ValueSet'
       });
       expect(pkg.fishForMetadata('Soups', Type.ValueSet)).toEqual(soupsValueSetByID);
       expect(
@@ -444,7 +449,8 @@ describe('Package', () => {
       expect(numericsCodeSystemByID).toEqual({
         id: 'numerics',
         name: 'Numbers',
-        url: 'http://hl7.org/fhir/us/minimal/CodeSystem/numerics'
+        url: 'http://hl7.org/fhir/us/minimal/CodeSystem/numerics',
+        resourceType: 'CodeSystem'
       });
       expect(pkg.fishForMetadata('Numbers', Type.CodeSystem)).toEqual(numericsCodeSystemByID);
       expect(
@@ -456,7 +462,8 @@ describe('Package', () => {
       const drSueInstanceById = pkg.fishForMetadata('dr-sue', Type.Instance);
       expect(drSueInstanceById).toEqual({
         id: 'dr-sue',
-        name: 'DrSue'
+        name: 'DrSue',
+        resourceType: 'Practitioner'
       });
       expect(pkg.fishForMetadata('DrSue', Type.Instance)).toEqual(drSueInstanceById);
     });
@@ -554,7 +561,8 @@ describe('Package', () => {
         name: 'Funny',
         sdType: 'Condition',
         url: 'http://hl7.org/fhir/us/minimal/StructureDefinition/fun-ny',
-        parent: 'http://hl7.org/fhir/StructureDefinition/Condition'
+        parent: 'http://hl7.org/fhir/StructureDefinition/Condition',
+        resourceType: 'StructureDefinition'
       });
       expect(pkg.fishForMetadata('Funny')).toEqual(funnyProfileByID);
       expect(
@@ -567,7 +575,8 @@ describe('Package', () => {
         name: 'PoorTaste',
         sdType: 'Extension',
         url: 'http://hl7.org/fhir/us/minimal/StructureDefinition/poor-taste',
-        parent: 'http://hl7.org/fhir/StructureDefinition/Extension'
+        parent: 'http://hl7.org/fhir/StructureDefinition/Extension',
+        resourceType: 'StructureDefinition'
       });
       expect(pkg.fishForMetadata('PoorTaste')).toEqual(poorTasteExtensionByID);
       expect(
@@ -580,7 +589,8 @@ describe('Package', () => {
         name: 'WheatBeer',
         sdType: 'wheat-beer',
         url: 'http://hl7.org/fhir/us/minimal/StructureDefinition/wheat-beer',
-        parent: 'http://hl7.org/fhir/StructureDefinition/Element'
+        parent: 'http://hl7.org/fhir/StructureDefinition/Element',
+        resourceType: 'StructureDefinition'
       });
       expect(pkg.fishForMetadata('WheatBeer')).toEqual(wheatBeerLogicalByID);
       expect(
@@ -593,7 +603,8 @@ describe('Package', () => {
         name: 'Destination',
         sdType: 'Destination',
         url: 'http://hl7.org/fhir/us/minimal/StructureDefinition/Destination',
-        parent: 'http://hl7.org/fhir/StructureDefinition/DomainResource'
+        parent: 'http://hl7.org/fhir/StructureDefinition/DomainResource',
+        resourceType: 'StructureDefinition'
       });
       expect(pkg.fishForMetadata('Destination')).toEqual(destinationResourceByID);
       expect(
@@ -604,7 +615,8 @@ describe('Package', () => {
       expect(soupsValueSetByID).toEqual({
         id: 'soup-flavors',
         name: 'Soups',
-        url: 'http://hl7.org/fhir/us/minimal/ValueSet/soup-flavors'
+        url: 'http://hl7.org/fhir/us/minimal/ValueSet/soup-flavors',
+        resourceType: 'ValueSet'
       });
       expect(pkg.fishForMetadata('Soups')).toEqual(soupsValueSetByID);
       expect(pkg.fishForMetadata('http://hl7.org/fhir/us/minimal/ValueSet/soup-flavors')).toEqual(
@@ -615,7 +627,8 @@ describe('Package', () => {
       expect(numericsCodeSystemByID).toEqual({
         id: 'numerics',
         name: 'Numbers',
-        url: 'http://hl7.org/fhir/us/minimal/CodeSystem/numerics'
+        url: 'http://hl7.org/fhir/us/minimal/CodeSystem/numerics',
+        resourceType: 'CodeSystem'
       });
       expect(pkg.fishForMetadata('Numbers')).toEqual(numericsCodeSystemByID);
       expect(pkg.fishForMetadata('http://hl7.org/fhir/us/minimal/CodeSystem/numerics')).toEqual(
@@ -625,7 +638,8 @@ describe('Package', () => {
       const drSueInstanceByID = pkg.fishForMetadata('dr-sue');
       expect(drSueInstanceByID).toEqual({
         id: 'dr-sue',
-        name: 'DrSue'
+        name: 'DrSue',
+        resourceType: 'Practitioner'
       });
 
       expect(pkg.fishForMetadata('DrSue')).toEqual(drSueInstanceByID);
diff --git a/test/fhirdefs/FHIRDefinitions.test.ts b/test/fhirdefs/FHIRDefinitions.test.ts
index 521ecae..12bc99b 100644
--- a/test/fhirdefs/FHIRDefinitions.test.ts
+++ b/test/fhirdefs/FHIRDefinitions.test.ts
@@ -379,7 +379,8 @@ describe('FHIRDefinitions', () => {
         name: 'Condition',
         sdType: 'Condition',
         url: 'http://hl7.org/fhir/StructureDefinition/Condition',
-        parent: 'http://hl7.org/fhir/StructureDefinition/DomainResource'
+        parent: 'http://hl7.org/fhir/StructureDefinition/DomainResource',
+        resourceType: 'StructureDefinition'
       });
       expect(
         defs.fishForMetadata('http://hl7.org/fhir/StructureDefinition/Condition', Type.Resource)
@@ -394,7 +395,8 @@ describe('FHIRDefinitions', () => {
         name: 'ELTSSServiceModel',
         sdType: 'eLTSSServiceModel',
         url: 'http://hl7.org/fhir/us/eltss/StructureDefinition/eLTSSServiceModel',
-        parent: 'http://hl7.org/fhir/StructureDefinition/Element'
+        parent: 'http://hl7.org/fhir/StructureDefinition/Element',
+        resourceType: 'StructureDefinition'
       });
       expect(
         defs.fishForMetadata(
@@ -412,7 +414,8 @@ describe('FHIRDefinitions', () => {
         name: 'boolean',
         sdType: 'boolean',
         url: 'http://hl7.org/fhir/StructureDefinition/boolean',
-        parent: 'http://hl7.org/fhir/StructureDefinition/Element'
+        parent: 'http://hl7.org/fhir/StructureDefinition/Element',
+        resourceType: 'StructureDefinition'
       });
       expect(
         defs.fishForMetadata('http://hl7.org/fhir/StructureDefinition/boolean', Type.Type)
@@ -427,7 +430,8 @@ describe('FHIRDefinitions', () => {
         name: 'Address',
         sdType: 'Address',
         url: 'http://hl7.org/fhir/StructureDefinition/Address',
-        parent: 'http://hl7.org/fhir/StructureDefinition/Element'
+        parent: 'http://hl7.org/fhir/StructureDefinition/Element',
+        resourceType: 'StructureDefinition'
       });
       expect(
         defs.fishForMetadata('http://hl7.org/fhir/StructureDefinition/Address', Type.Type)
@@ -442,7 +446,8 @@ describe('FHIRDefinitions', () => {
         name: 'observation-vitalsigns',
         sdType: 'Observation',
         url: 'http://hl7.org/fhir/StructureDefinition/vitalsigns',
-        parent: 'http://hl7.org/fhir/StructureDefinition/Observation'
+        parent: 'http://hl7.org/fhir/StructureDefinition/Observation',
+        resourceType: 'StructureDefinition'
       });
       expect(defs.fishForMetadata('observation-vitalsigns', Type.Profile)).toEqual(vitalSignsByID);
       expect(
@@ -461,7 +466,8 @@ describe('FHIRDefinitions', () => {
         name: 'mothersMaidenName',
         sdType: 'Extension',
         url: 'http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName',
-        parent: 'http://hl7.org/fhir/StructureDefinition/Extension'
+        parent: 'http://hl7.org/fhir/StructureDefinition/Extension',
+        resourceType: 'StructureDefinition'
       });
       expect(defs.fishForMetadata('mothersMaidenName', Type.Extension)).toEqual(
         maidenNameExtensionByID
@@ -482,7 +488,8 @@ describe('FHIRDefinitions', () => {
       expect(allergyStatusValueSetByID).toEqual({
         id: 'allergyintolerance-clinical',
         name: 'AllergyIntoleranceClinicalStatusCodes',
-        url: 'http://hl7.org/fhir/ValueSet/allergyintolerance-clinical'
+        url: 'http://hl7.org/fhir/ValueSet/allergyintolerance-clinical',
+        resourceType: 'ValueSet'
       });
       expect(defs.fishForMetadata('AllergyIntoleranceClinicalStatusCodes', Type.ValueSet)).toEqual(
         allergyStatusValueSetByID
@@ -503,7 +510,8 @@ describe('FHIRDefinitions', () => {
       expect(allergyStatusCodeSystemByID).toEqual({
         id: 'allergyintolerance-clinical',
         name: 'AllergyIntoleranceClinicalStatusCodes',
-        url: 'http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical'
+        url: 'http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical',
+        resourceType: 'CodeSystem'
       });
       expect(
         defs.fishForMetadata('AllergyIntoleranceClinicalStatusCodes', Type.CodeSystem)
@@ -642,7 +650,8 @@ describe('FHIRDefinitions', () => {
         name: 'Condition',
         sdType: 'Condition',
         url: 'http://hl7.org/fhir/StructureDefinition/Condition',
-        parent: 'http://hl7.org/fhir/StructureDefinition/DomainResource'
+        parent: 'http://hl7.org/fhir/StructureDefinition/DomainResource',
+        resourceType: 'StructureDefinition'
       });
       expect(defs.fishForMetadata('http://hl7.org/fhir/StructureDefinition/Condition')).toEqual(
         conditionByID
@@ -655,7 +664,8 @@ describe('FHIRDefinitions', () => {
         name: 'boolean',
         sdType: 'boolean',
         url: 'http://hl7.org/fhir/StructureDefinition/boolean',
-        parent: 'http://hl7.org/fhir/StructureDefinition/Element'
+        parent: 'http://hl7.org/fhir/StructureDefinition/Element',
+        resourceType: 'StructureDefinition'
       });
       expect(defs.fishForMetadata('http://hl7.org/fhir/StructureDefinition/boolean')).toEqual(
         booleanByID
@@ -668,7 +678,8 @@ describe('FHIRDefinitions', () => {
         name: 'Address',
         sdType: 'Address',
         url: 'http://hl7.org/fhir/StructureDefinition/Address',
-        parent: 'http://hl7.org/fhir/StructureDefinition/Element'
+        parent: 'http://hl7.org/fhir/StructureDefinition/Element',
+        resourceType: 'StructureDefinition'
       });
       expect(defs.fishForMetadata('http://hl7.org/fhir/StructureDefinition/Address')).toEqual(
         addressByID
@@ -681,7 +692,8 @@ describe('FHIRDefinitions', () => {
         name: 'observation-vitalsigns',
         sdType: 'Observation',
         url: 'http://hl7.org/fhir/StructureDefinition/vitalsigns',
-        parent: 'http://hl7.org/fhir/StructureDefinition/Observation'
+        parent: 'http://hl7.org/fhir/StructureDefinition/Observation',
+        resourceType: 'StructureDefinition'
       });
       expect(defs.fishForMetadata('observation-vitalsigns')).toEqual(vitalSignsProfileByID);
       expect(defs.fishForMetadata('http://hl7.org/fhir/StructureDefinition/vitalsigns')).toEqual(
@@ -695,7 +707,8 @@ describe('FHIRDefinitions', () => {
         name: 'mothersMaidenName',
         sdType: 'Extension',
         url: 'http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName',
-        parent: 'http://hl7.org/fhir/StructureDefinition/Extension'
+        parent: 'http://hl7.org/fhir/StructureDefinition/Extension',
+        resourceType: 'StructureDefinition'
       });
       expect(defs.fishForMetadata('mothersMaidenName')).toEqual(maidenNameExtensionByID);
       expect(
@@ -708,7 +721,8 @@ describe('FHIRDefinitions', () => {
       expect(allergyStatusValueSetByID).toEqual({
         id: 'allergyintolerance-clinical',
         name: 'AllergyIntoleranceClinicalStatusCodes',
-        url: 'http://hl7.org/fhir/ValueSet/allergyintolerance-clinical'
+        url: 'http://hl7.org/fhir/ValueSet/allergyintolerance-clinical',
+        resourceType: 'ValueSet'
       });
       expect(defs.fishForMetadata('AllergyIntoleranceClinicalStatusCodes')).toEqual(
         allergyStatusValueSetByID
@@ -721,7 +735,8 @@ describe('FHIRDefinitions', () => {
       expect(w3cProvenanceCodeSystemByID).toEqual({
         id: 'w3c-provenance-activity-type',
         name: 'W3cProvenanceActivityType',
-        url: 'http://hl7.org/fhir/w3c-provenance-activity-type'
+        url: 'http://hl7.org/fhir/w3c-provenance-activity-type',
+        resourceType: 'CodeSystem'
       });
       expect(defs.fishForMetadata('W3cProvenanceActivityType')).toEqual(
         w3cProvenanceCodeSystemByID
@@ -737,7 +752,8 @@ describe('FHIRDefinitions', () => {
         name: 'ELTSSServiceModel',
         parent: 'http://hl7.org/fhir/StructureDefinition/Element',
         sdType: 'eLTSSServiceModel',
-        url: 'http://hl7.org/fhir/us/eltss/StructureDefinition/eLTSSServiceModel'
+        url: 'http://hl7.org/fhir/us/eltss/StructureDefinition/eLTSSServiceModel',
+        resourceType: 'StructureDefinition'
       });
       expect(defs.fishForMetadata('ELTSSServiceModel')).toEqual(eLTSSServiceModelByID);
       expect(
diff --git a/test/import/FSHTank.test.ts b/test/import/FSHTank.test.ts
index f0cd519..af93f3f 100644
--- a/test/import/FSHTank.test.ts
+++ b/test/import/FSHTank.test.ts
@@ -309,36 +309,42 @@ describe('FSHTank', () => {
       id: 'prf1',
       name: 'Profile1',
       url: 'http://hl7.org/fhir/us/minimal/StructureDefinition/prf1',
-      parent: 'Observation'
+      parent: 'Observation',
+      resourceType: 'StructureDefinition'
     };
     const ext1MD: Metadata = {
       id: 'ext1',
       name: 'Extension1',
       url: 'http://hl7.org/fhir/us/minimal/StructureDefinition/ext1',
-      parent: 'Extension2'
+      parent: 'Extension2',
+      resourceType: 'StructureDefinition'
     };
     const log1MD: Metadata = {
       id: 'log1',
       name: 'Logical1',
       url: 'http://hl7.org/fhir/us/minimal/StructureDefinition/log1',
       sdType: 'http://hl7.org/fhir/us/minimal/StructureDefinition/log1',
-      parent: 'Element'
+      parent: 'Element',
+      resourceType: 'StructureDefinition'
     };
     const res1MD: Metadata = {
       id: 'res1',
       name: 'Resource1',
       parent: 'DomainResource',
-      url: 'http://hl7.org/fhir/us/minimal/StructureDefinition/res1'
+      url: 'http://hl7.org/fhir/us/minimal/StructureDefinition/res1',
+      resourceType: 'StructureDefinition'
     };
     const vs1MD: Metadata = {
       id: 'vs1',
       name: 'ValueSet1',
-      url: 'http://hl7.org/fhir/us/minimal/ValueSet/vs1'
+      url: 'http://hl7.org/fhir/us/minimal/ValueSet/vs1',
+      resourceType: 'ValueSet'
     };
     const cs1MD: Metadata = {
       id: 'cs1',
       name: 'CodeSystem1',
-      url: 'http://hl7.org/fhir/us/minimal/CodeSystem/cs1'
+      url: 'http://hl7.org/fhir/us/minimal/CodeSystem/cs1',
+      resourceType: 'CodeSystem'
     };
     const inst1MD: Metadata = {
       id: 'inst1',
@@ -671,7 +677,8 @@ describe('FSHTank for HL7', () => {
       name: 'HL7Logical',
       url: 'http://hl7.org/fhir/StructureDefinition/hl7-log',
       sdType: 'hl7-log',
-      parent: 'Element'
+      parent: 'Element',
+      resourceType: 'StructureDefinition'
     };
 
     it('should find valid HL7 fish metadata when fishing by id for logical models', () => {
diff --git a/test/utils/MasterFisher.test.ts b/test/utils/MasterFisher.test.ts
index 2d4abd2..3c6bbe4 100644
--- a/test/utils/MasterFisher.test.ts
+++ b/test/utils/MasterFisher.test.ts
@@ -70,7 +70,8 @@ describe('MasterFisher', () => {
       name: 'Profile1',
       sdType: 'Procedure',
       url: 'http://hl7.org/fhir/us/minimal/StructureDefinition/prf1',
-      parent: 'Procedure'
+      parent: 'Procedure',
+      resourceType: 'StructureDefinition'
     });
   });
 
@@ -84,7 +85,8 @@ describe('MasterFisher', () => {
       name: 'Profile2',
       sdType: 'Observation',
       url: 'http://hl7.org/fhir/us/minimal/StructureDefinition/Profile2',
-      parent: 'bp'
+      parent: 'bp',
+      resourceType: 'StructureDefinition'
     });
   });
 
@@ -104,7 +106,8 @@ describe('MasterFisher', () => {
       name: 'Practitioner',
       sdType: undefined,
       url: 'http://hl7.org/fhir/us/minimal/StructureDefinition/my-dr',
-      parent: 'Practitioner'
+      parent: 'Practitioner',
+      resourceType: 'StructureDefinition'
     });
   });
 
@@ -119,7 +122,8 @@ describe('MasterFisher', () => {
       name: 'Profile3',
       sdType: 'Condition',
       url: 'http://hl7.org/fhir/us/minimal/StructureDefinition/profile3',
-      parent: 'http://hl7.org/fhir/StructureDefinition/Condition'
+      parent: 'http://hl7.org/fhir/StructureDefinition/Condition',
+      resourceType: 'StructureDefinition'
     });
   });
 
@@ -135,7 +139,8 @@ describe('MasterFisher', () => {
       name: 'Patient',
       sdType: 'Patient',
       url: 'http://hl7.org/fhir/StructureDefinition/Patient',
-      parent: 'http://hl7.org/fhir/StructureDefinition/DomainResource'
+      parent: 'http://hl7.org/fhir/StructureDefinition/DomainResource',
+      resourceType: 'StructureDefinition'
     });
   });
 
@@ -150,7 +155,8 @@ describe('MasterFisher', () => {
       name: 'MyVitalSigns',
       sdType: 'Observation',
       url: 'http://hl7.org/fhir/us/minimal/StructureDefinition/vitalsigns',
-      parent: 'http://hl7.org/fhir/StructureDefinition/Observation'
+      parent: 'http://hl7.org/fhir/StructureDefinition/Observation',
+      resourceType: 'StructureDefinition'
     });
   });
 
@@ -170,7 +176,8 @@ describe('MasterFisher', () => {
       name: fhirDefinedVitalSigns.name,
       sdType: fhirDefinedVitalSigns.type,
       url: fhirDefinedVitalSigns.url,
-      parent: fhirDefinedVitalSigns.baseDefinition
+      parent: fhirDefinedVitalSigns.baseDefinition,
+      resourceType: 'StructureDefinition'
     });
     defs.resetPredefinedResources();
   });
@@ -185,7 +192,8 @@ describe('MasterFisher', () => {
       name: 'Organization',
       sdType: 'Organization',
       url: 'http://hl7.org/fhir/us/minimal/StructureDefinition/my-org',
-      parent: 'http://hl7.org/fhir/StructureDefinition/Organization'
+      parent: 'http://hl7.org/fhir/StructureDefinition/Organization',
+      resourceType: 'StructureDefinition'
     });
   });