Showing posts with label FHIR. Show all posts
Showing posts with label FHIR. Show all posts

Sunday, March 8, 2026

Some thoughts while working with HAPI FHIR

As far as I'm concerned, HAPI on FHIR is THE way to go when working with FHIR Resources in Java.  However, one of challenges I've had with HAPI is having to write conditional code based on the type of resource I'm working with when I'm handling similar fields, for example setPatient/getPatient/hasPatient or setSubject/getSubject/hasSubject.  A number of FHIR Resources have these common setter/getter patterns, and not just with references.  Another example is for primitive fields, such as dateTime.

This makes writing automation code somewhat difficult.

I'd love to see some interfaces defined for common 5W patterns that are commonly used on FHIR Resources be introduced that encapsulate the set/get/has methods.  These interfaces could be named using the pattern: HasX or HasXList, where X is the field name.  Some examples of these follow:

public interface HasSubject {
   HasSubject setSubject(Reference subject);
   Reference getSubject();
   boolean hasSubject();
}

public interface HasOrganizationList {
   HasOrganizationList addOrganization(Reference);
   Reference addOrganization();
   List<Reference> getOrganization();
   HasOrganizationList setOrganization(List<Reference> orgList);
   Reference getOrganizationFirstRep();
}

This would mean that I could use:

if (x instanceof HasSubject hs) {
   hs.setSubject(patient);
} else if (x instanceof HasPatient hp) {
   hp.setPatient(patient);
}

Which would make certain automation related code a lot easier.

You might need to make the interfaces use a template parameter for the type to address certain differences in name use, e.g., HasEffectiveTime<DateTimeType> or HasEffectiveTime<InstantType>.



Wednesday, January 7, 2026

HL7 V2 to FHIR

You may have seen my guest post on Healthcare IT Today about Why Public Health Data needs to Modernize a few months back.  I've been working to support this for years, and in October 2025, HL7 achieved a pretty significant goal supporting that.  The HL7 Version 2 to FHIR Standard for Trial Use was finally published.

I have been working on the HL7 Version 2 to FHIR project since the initial inception in late 2018, almost at the same time as I started at Audacious Inquiry.  My first project at Audacious was to create a V2 to FHIR Converter, which we did build and provide to some of our customers.  Audacious contributed the mappings our team developed (mostly through efforts of our product owner and me, with some help from our HL7 V2 interface developers) to the project in early 2019, and they became the initial spreadsheets that were used to create the HL7 V2 to FHIR guide.

I now have the somewhat dubious attribute as having be an editor working on one of the longest running projects from inception to STU publication in HL7 history (7 years).  Yes, I am certain some have run longer, but none that I can think of off the top of my head.  Part of my editorial role in the early days was to have evolved the effort to represent the content in spreadsheets, and then to translate (in code), those nearly 400 spreadsheets into over 250 FHIR ConceptMap resources.  More recently, it has been maintenance of the V2 to FHIR IG generator code base, and detailed technical review of the content produced in the guide to verify that it can be accessed in computable form, not just via spreadsheets but through the FHIR Resources, and ensuring that all content is consistently handled and can be used to automatically generate a V2 to FHIR converter.

I'm thrilled to see this finally published, and our team has been working on turning the output of this guide into a Data Modernization offering for public health that will enable them to turn legacy data into FHIR resources that can be accessed through modern APIs.  We are working on a completely new software base to support V2 and CDA to FHIR conversions we'll be talking more about at HIMSS 26.


Wednesday, July 9, 2025

A syntax for validating V2toFHIR Conversions

I've been working on V2toFHIR since its inception.  A month ago, and back in January I participated in two Connectathons that are using that capability from this code.  In building that engine, I needed a way to test a conversion of an HL7 message in a way that would make it easier for me to write detailed tests.

The input to a test is an HL7 V2 message with segments.  A very long time ago, I resolved to use plain-text files containing messages and divide them with a blank line.  Here's an example below of a file with two messages I've previously used for other purposes at IHE Connectathons (I have several years of IHE Connectathon test data lying about):

# Outbound ack:

MSH|^~\&|PAT_IDENTITY_X_REF_MGR_MISYS_TLS|ALLSCRIPTS|OTHER_IBM_BRIDGE_TLS|IBM|20090224114149-0500||ACK^A04|OpenPIXPDQ10.243.0.65.19767899354465|P|2.3.1

MSA|AA|0851077658473390286


# PIX Feed (ADT^A01)

# Inbound feed:

MSH|^~\&|OTHER_IBM_BRIDGE_TLS|IBM|PAT_IDENTITY_X_REF_MGR_MISYS|ALLSCRIPTS|20090224104152-0600||ADT^A01^ADT_A01|8686183982575368499|P|2.3.1

EVN||20090224104152-0600

PID|||102^^^IBOT&1.3.6.1.4.1.21367.2009.1.2.370&ISO||SINGLETON^MARION||19661109|F

PV1||I


You might also note that I allow for comment characters at the start of the line. My test framework simply ignores those lines while reading the test data, and if gives me a way to identify the test case. For testing V2 to FHIR Conversions, I needed a way to create assertions that would be used to verify the assertion.  Of course, FHIRPath comes to mind because it's already an expression language widely used in FHIR itself for assertions and other manipulations of FHIR Resources.  That's ideal language for my use case.


So, I added a capability in my test case reader to add assertions to the test case. Here's an example for the first test case above.  The @ sign introduces an assertion.


@MessageHeader.source.name = "PAT_IDENTIFY_X_REF_MGR_MISYS_TLS"

@MessageHeader.source.endpoint = "ALLSCRIPTS"

@MessageHeader.destination.name = "OTHER_IBM_BRIDGE_TLS"

@MessageHeader.destination.endpoint = "IBM"


I'm actually checking a bundle but use a little string substitution hackery to replace MessageHeader with %context.entry.resource.ofType(MessageHeader) at the head of the expression to save me about 31 characters of typing each time.


The next bit of fun is enabling the assertions to come close to where they are useful in the message.  It should be obvious that each test case has two streams of data, the message, and the assertions.  They can easily be interwoven.  So, assertions can easily follow the segment, but I also wanted more, because some segments can be very long.  Borrowing from other scripting languages (e.g., bash), I allow the introduction of the \ character at the end of the line to allow lines to be continued.  I realized that it would be important to visually see the start an end of a segment in the file, so I ignore leading whitespace at the beginning of continuation lines.  So now I can write:


# Outbound ack:

MSH|^~\&|\

PAT_IDENTITY_X_REF_MGR_MISYS_TLS|\

@MessageHeader.source.name = "PAT_IDENTIFY_X_REF_MGR_MISYS_TLS"

ALLSCRIPTS|\

@MessageHeader.source.endpoint = "ALLSCRIPTS"

OTHER_IBM_BRIDGE_TLS|\

@MessageHeader.destination.name = "OTHER_IBM_BRIDGE_TLS"

IBM|20090224114149-0500|\

@MessageHeader.destination.endpoint = "IBM"

|ACK^A04|OpenPIXPDQ10.243.0.65.19767899354465|P|2.3.1

MSA|AA|0851077658473390286


And so forth.  In this way, I now have a test case where I can start with the message in my usual form (simple text with a blank line between cases).  From there I can augment the test case with assertions that I feel are important.  Following that, I can also break things up, even just long lines of V2 messages to make the test files more readable.


The final piece of this was to allow long assertions to be broken up the same way messages are, and to steal the final comment in the assertion expression (comments are part of FHIRPath syntax) to use for my assertion message in my testing framework.  That's also just simple string substitution as well.


You can find all of the code for this in V2toFHIR, the open-source converter my team and I created for some of our ongoing work.  The two key files of interest are MessageParserTests.java and TestData.java.  The method TestData.load() is where all the test input parsing magic happens.  It's not really that complicated.




Tuesday, July 23, 2024

Is MVX a FHIR CodingSystem or an IdentifierSystem

Well, if you look it up, it's a Coding System, and it's published by CDC here.  But in fact, it does both.  It identifies the concepts of Vaccine Manufacturers, but those are also "Entities" in V3 speak, actual things you could put your hands on evidence of (e.g., articles of incorporation), if still conceptual.  So they also serve as identifiers.

When I used to teach CDA (and V3) regularly, I would explain that codes are simply identifiers for concepts.  I ran across this while working on V2 to FHIR because FHIR Immunization resources treat the manufacturer of an immunization as a first-class resource, an organization, but HL7 V2 treats it as a code from a table in a CE or CWE datatype (depending on HL7 version).

What is one to do?  Well, it's actually pretty straightforward.  I downloaded the CVC MVX codes from the link above, write them into a coding system with code being the MVX code, and display being the Manufacturer name.  When I create the Organization, name gets the value of display, and identifier gets the value of code, and system remains the same.  It works like a charm.

     Keith


Thursday, June 27, 2024

The true edge cases of date/time parsing

 I'm in the process of developing a Java library to implement the V2-to-FHIR datatype conversions.  This is a core component for a V2 to FHIR converter I hope to open source at some point.

I'm using HAPI V2 and HAPI FHIR because Java, and these are the best implementations in Java.

Some interesting learnings:

  1.  HAPI  FHIR and HAPI V2 have overlapping acceptable input ranges.
    In order to provide the broadest support, I'm actually using both.  The V2 parser is easier to manage with the range of acceptable ISO-8601 date time strings, since it's easier to remove any existing punctuation from an ISO-8601 data type (the -, : and T characters that typically show up in XML and JSON time stamps), than it is to insert missing punctuation, so I go with that first with the input string normalized to the 8601 format w/o punctuation.
  2. There are a couple of wonky cases that HAPI FHIR parses but V2 doesn't.  If the hours, minutes or seconds are set to the string "-0", the strings parse in FHIR, but don't in V2.  I'm guessing someone is using parse by punctuation and that Integer.toString("-0") and getting 0 in the FHIR case.
  3. Another interesting case: calling new DateTimeType(s).getValueAsString() doesn't return the same string as the following in all cases (e.g., the "2024-06-25T-0:00:00" case).
         DateTimeType original = DateTimeType(s);
    DateTimeType normalized = new DateTimeType(original.getValue(), 
      original.getPrecision());
         assertEquals(original, normalized);
    This is because HAPI FHIR doesn't renormalize the input string if it is able to be parsed (but it probably should).  I've ensured my converter class does this renormalizing.  
  4. If a time zone isn't present, then it's assumed to be the local time zone in both HAPI V2 and HAPI FHIR.  This presents small problems in testing, but nothing insurmountable.
  5. While the value of 60 is allowed in seconds in FHIR, what HAPI FHIR actually does is role to the next minute.  Again, very edge case, as leap second events are infrequent in real life (only 27 since 1972), and even more so as detected as having impacts during code execution.  GPS aware applications have to deal with them because GPS time doesn't care about leap seconds (although your phone, which synchronizes it's time via GPS may correct for them).

For what it's worth as a side note, issue # 3 above happens with other types such as IntegerType, for example, try this test:
|
assertEquals(new IntegerType("01").getValueAsString(),
             new IntegerType(1).getValueAsString());

What I've done in my converter is after creating a value from a string, ensuring I perform any string-based construction like this:

IntegerType i = new IntegerType("01"); 
i.setValue(i.getValue());

This ensure that I get the expected string representation.  This helps resolve issues with leading zeros or plus signs (e.g., -01, 01, +01, +2).


Tuesday, March 28, 2023

Translating CDA to FHIR: SNOMED CT Codes


One of the challenges in translating from CDA (or C-CDA) to FHIR is related to variations in how CDA and FHIR store complex SNOMED-CT codes using qualifiers.  As you might expect, these differences are especially significant when the concept being encoded uses the SNOMED CT Expressions

In CDA, these codes are stored using the ConceptDescriptor (CD) data type, and unpacks the concept using the Qualifier component of the concept description when the code is a SNOMED CT expression.  This is arguably a flaw in HL7 Version 3 standards, as the qualifier concept is variably expressed in SNOMED CT and other terminology systems differently through their own expression languages, and shouldn't be syntactically unpacked in the CD data type.  It's overengineering the terminology component to meet the needs of a data model, and overgeneralizing (in the favor of SNOMED) how this may be handled in other terminologies.

HL7 FHIR uses the CodableConcept data type, and doesn't unpack the expression (fixing this flaw).  If we take the SNOMED CT example provided at that link and shown below:

Example

And look at it in CDA, we would see:

<code xsi:type="CD" code="284196006" codeSystem="2.16.840.1.113883.6.96" 
      displayName="burn of skin">
    <qualifier>
        <name code="363698007" codeSystem="2.16.840.1.113883.6.96" displayName="finding site" />
        <value code="770850006" codeSystem="2.16.840.1.113883.6.96" 
               displayName="Skin structure of left index finger"/>
    </qualifier>
</code>

But in FHIR:

<code>
  <coding>
    <code value='284196006|burn of skin|:363698007|finding site|=770850006|Skin structure of left index finger|' 
          system='http://snomed.info/sct'/>
  </coding>
</code>
                    <!-- OR Better yet (IMNSHO*) -->
<code>
  <coding>
    <code value='284196006:363698007=770850006' system='http://snomed.info/sct'
          display='|burn of skin|:|finding site|=|Skin structure of left index finger|'/>
  </coding>
</code>
NOTE, the latter example is just a simplified version of the former without any display name values.

This use of SNOMED CT Expressions is advanced, and doesn't show up often in the real world, but when it does, it is a real challenge for implementers.

To convert from a SNOMED CT code with qualifiers in CDA to a SNOMED CT Code in FHIR, apply the following algorithm:

  1. Generate new nested <fhir:code>, <fhir:coding> and <fhir:code> elements.
  2. If there is a cda:code/@code attribute, generate a value attribute in the final fhir:code element as follows:
    1. Set fhir:code/@value attribute in FHIR to the value of cda:code/@code
    2. (Optional and legal, but not recommended): If there is a cda:code/@displayName attribute in the CDA, append "|" + @displayName + "|" to fhir:code/@value.  NOTE: This captures displayNames, which you should only trust from your own terminology service, and it also appends a string to fhir:code/@value which will complicate FHIR Search operations in most FHIR implementations.
    3. For each cda:qualifier element from SNOMED CT in the cda:code element. 
      1. Append ":" + cda:qualifer/cda:name/@code to fhir:code/@value
      2. Again, optional, legal and not recommended: append "|" + @displayName + "|" to fhir:code/@value
      3. Append "=" + cda:qualifer/cda:value/@code to fhir:code/@value
  3. If there isn't a code/@code attribute, generate an appropriate uncoded value in FHIR (possibly using null flavors depending on the IG).  NOTE: If there's NO @code, but there are cda:qualifier elements, this is bogus, and should be reported as a validation error on input, at least for SNOMED CT.  You can't qualify an uncoded value.
Some comments:
  1. SNOMED CT expressions allow display name values to be appended to a code between vertical bars | (we call these pipes in HL7 V2).  I don't recommend putting the display names into the code even though it's legal and semantically correct.  Pragmatically, it's going to cause your FHIR implementation to have problems in search.
    1. This is legal:
      <code value='284196006|burn of skin|:363698007|finding site|=770850006|Skin structure of left index finger|'  system='http://snomed.info/sct'/>
    2. But this is MUCH better:
      <code value='284196006:363698007=770850006'
            display='|burn of skin|:|finding site|=|Skin structure of left index finger|'
            system='http://snomed.info/sct'/>

  2. Don't trust display name values from external systems.  Use the display name values from your own, validated terminology service.

  3. Consider how incorporating qualifiers into codes will impact your search.  In general, qualifiers reflect refinement in SNOMED CT, which implies subclasses of the core code.  You might consider coding it twice, once without the qualifiers, and a second time with the qualifiers:
    <code>
      <coding>
    
        <code value='284196006' system='http://snomed.info/sct'
              display='burn of skin'/>
        <code value='284196006:363698007=770850006' system='http://snomed.info/sct'
    display='|burn of skin|:|finding site|=|Skin structure of left index finger|'/> </coding> </code>
    I like this better because a search for fhir/Condition?code=
    http://snomed.info/sct|284196006 is going to find what the end user expects, all Condition resources representing a burn of skin.

  4. If you go with #3 above, you may be concerned (and rightly so) about negation.  There are a couple of recommendations you might consider:
    1. Insert a human into the mix and ensure that qualifiers related to negation or other situations with explicit context are correctly interpreted in workflows that involve importing data from CDA into the patient chart.
    2. Read through and understand SNOMED CT documentation on this topic.
    3. Few systems today use SNOMED CT expressions.  Fewer yet use them with negation.  That doesn't mean you can safely ignore it. Make it an exceptional process in your conversions, AND don't try to automate processing it, just detect it and call for (human) assistance in interpretation.  The number of times that happens is likely be small enough to avoid user complaint, yet remain safe for patient care.  And note, my expert opinion does not excuse you from making your own risk assessments and code accordingly.

Keith

P.S. To make it even uglier, technically, the FHIR representation would also be legal in CDA, if about as rare as a snowball in Central Africa.

<code xsi:type="CD" value='284196006:363698007=770850006'
      displayName='|burn of skin|:|finding site|=|Skin structure of left index finger|'
      codeSystem="2.16.840.1.113883.6.96">
* IMNSHO = In My Not So Humble Opinion

Monday, February 21, 2022

Automating FHIR IG Generation from Non-FHIR Sources by cloning myself

An awesome lot of my FHIR IG development involves taking data in one structure and converting it to other structured formats required by the FHIR IG Development process.

That includes creating .fsh and .md files from structured data.

I do most of my FHIR IG development in Eclipse (because if it involves code or transforms, that's where my XML Editors plug in, or my Java transform code for other formats).  I've narrowed down my processes so that I can run Sushi, the FHIR Build or my "custom build script" from within eclipse with one of three external tool configuration scripts:

Here's my normal "Build" script, which just launches build.bat in the project root folder:

<launchConfiguration type="org.eclipse.ui.externaltools.ProgramLaunchConfigurationType">
    <stringAttribute key="org.eclipse.debug.ui.ATTR_CONSOLE_ENCODING" value="UTF-8"/>
    <stringAttribute key="org.eclipse.ui.externaltools.ATTR_LAUNCH_CONFIGURATION_BUILD_SCOPE" value="${project}"/>
    <stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="C:\Windows\System32\cmd.exe"/>
    <stringAttribute key="org.eclipse.ui.externaltools.ATTR_TOOL_ARGUMENTS" value="/c build.bat"/>
    <stringAttribute key="org.eclipse.ui.externaltools.ATTR_WORKING_DIRECTORY" value="${project_loc}"/></launchConfiguration>

Sometimes I just want to rebuild the IG, for that I can just call the IG Publisher:

<launchConfiguration type="org.eclipse.ui.externaltools.ProgramLaunchConfigurationType">
    <stringAttribute key="org.eclipse.debug.ui.ATTR_CONSOLE_ENCODING" value="UTF-8"/>
    <stringAttribute key="org.eclipse.ui.externaltools.ATTR_LAUNCH_CONFIGURATION_BUILD_SCOPE" value="${project}"/>
    <stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="C:\jdk-11.0.2\bin\java.exe"/>
    <stringAttribute key="org.eclipse.ui.externaltools.ATTR_TOOL_ARGUMENTS" value="-Xmx2G -jar ../publisher.jar -ig ig.ini -tx n/a"/>
    <stringAttribute key="org.eclipse.ui.externaltools.ATTR_WORKING_DIRECTORY" value="${project_loc}"/>
</launchConfiguration>

And sometimes I just want to rerun Sushi alone, without running FHIR:

<launchConfiguration type="org.eclipse.ui.externaltools.ProgramLaunchConfigurationType">
    <stringAttribute key="org.eclipse.debug.ui.ATTR_CONSOLE_ENCODING" value="UTF-8"/>
    <stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="C:\Program Files\nodejs\node.exe"/>
    <stringAttribute key="org.eclipse.ui.externaltools.ATTR_TOOL_ARGUMENTS" value="C:\Users\kboone\AppData\Roaming\npm\node_modules\fsh-sushi\dist\app.js fsh -o ."/>
    <stringAttribute key="org.eclipse.ui.externaltools.ATTR_WORKING_DIRECTORY" value="${project_loc}"/>
</launchConfiguration>

Save any of these snipped to a file named your-file-name.launch and you should be able to import it as an external tool configuration.

I've even gone so far (finally) as to make this build process an automated part of my check-ins to the FHIR Repository (that way I don't need to be present to run the scripts when source material changes).  What I usually do is take the command line (Windows) script and convert it to Unix and run it in GitHub CI/CD.  For V2-to-FHIR, I just recently created a CI/CD workflow that updates the Sushi from CSV files downloaded from Google Sheets.  This is all for publication purposes.  For SANER, I had other tools to create the SANER IG from an XML file I've been using for IG creation for IHE profiles.  Effectively what I'm doing is cloning myself using GitHub CICD.  I'm not even the first person in the community to do this.  It's basically how build.fhir.org runs, essentially as Grahame in-a-box, but it scales and neither I nor Grahame do.

We were running that up until a few weeks before final publication, after which we did all edits to the files created by the automated process, and removed all the code that supported that process.  That's because the FSH and FHIR IG content in GitHub are the official sources for the IG, not the process inputs used to generate them, and I don't want to be on the hook for maintenance for life for my own tooling that I'm using MOSTLY to make my life easier.

I've build now about 5 different guides using automated FHIR IG (and FSH generation tools).  It's the only way to fly when you have to turn dozens of structured inputs into FHIR IG outputs.


Thursday, October 14, 2021

Responding before reading @alissaknight's: Playing with FHIR: Hacking and Securing FHIR APIs

If you've been sitting under a rock, you missed the latest big explosion in the FHIR World, Alissa Knight's most recent report: Playing with FHIR: Hacking and Securing FHIR APIs.  I've downloaded it, and looked at the fantastic graphics (e.g., image right), but I've not really read it yet.  This is the kind of report that will require as deep a read I suspect as some Federal regulations.  If I get the time (perhaps this weekend), I may even do a tweet-through-read-through.

I have some initial thoughts based on my reading of the tweets in this stream:

Here's what I expect to find, noting that this is only my guess as to what is happening:

Various apps rely on a FHIR backend which will allow a replay attack to be performed whereby:

  1. The attacker obtains access to the authorization token used by the FHIR API call to make other API calls on a different patient. 

    NOTE: There are a number of ways to obtain this authorization token depending on how the application is constructed and the level of access one has to developer tools, and application hardware and software.  Assume that the hostile attacker is one in a million that has access to all of that, not the common high-school student (but don't count them out either, some of them are that good).  Is ia a Java or .Net app?  There's a debugger for that, and I can almost assuredly final all of your assemblies or jar files on my device emulator, and debug code running in a device.  Did you obfuscate your code?  If not, I can reverse compile it, there are tools for that too, and even obfuscation is not enough when you consider that platform calls are still going to be obvious, and all the important ones are well known, so I can work back the stack to the code I need to manually investigate.

  2. The attacker constructs new API calls to make request using the same authorization token.

  3. The call succeeds because the only check that is performed on the authorization token by the back end server is that it is a valid token issued by the appropriate authorizer.
The fix is not simple.  Basically, the back end developer needs to bind the access control restrictions associated with the original access request to obtain the authorization token to the subset of data that it authorizes access to (this is the easy part), and enforce them on every request (this is the hard part), incoming and outgoing (unless you can prove the outgoing data will match the authorization based on how the query works).  JWT tokens provide more than adequate capability to bind exactly those details in an easily accessible way.  Essentially, the claims in the JWT should indicate what is allowed to be accessed, when, how, and by whom.  Sadly for my first FHIR Server, JWT was still a work in progress, but I simply encoded an index in the token which pointed to memorialized claims in a database, constructed when the token was first created.

Once you can do that much, fine-grained access control is actually quite simple.  That was originally considered to be out of scope in my first effort for reasons of cost and complexity, but because we had to build the right security infrastructure even to support coarse-grained access control, fine-grained control simply became just a little bit harder, but also a worthwhile differentiator from other offerings.

As I said earlier, my guess as to what is wrong is only supposition, an experienced, wild-assed guess at what I will find upon reading.  But if I can guess it before reading, then arguably, so can someone else as smart as I without barely even an incentive.  Consider also that intelligence and quality of character are not necessarily correlated, nor do they even need to exist in the same body for a vulnerability such as I described above to be readily and easily exploited by someone with enough motive, and knowledge about how to use tools that people smarter than them wrote.

In the world of the great world wide web, FHIR API servers, Java, .net or other platform, the basic concepts of servlets, intercepters, filters, hooks, cut points or similar concepts by other names in non-Java platforms all exist and are able to perform these checks and validations both before and after the call completes, so that you can:
  1. Verify that what is being asked for is allowed to be asked for (never assume that the querant of your back end can only be your application)
  2. The data that is being returned also matches what the authorization allows the end user to see, and either filter, or simply reject the request (after having performed some work that you wish you hadn't).
I led teams building (writing some of the code myself) commercial FHIR servers on top of readily available open source frameworks a number of times in my life (most are still in use), and I've also implemented other API interfaces pre-FHIR.  When I built in security controls, I bound the security tokens to the specifically authorized purpose and sources, and I didn't trust that token to be used only for that purpose, I made sure it was verified every time.

I have to tell you that was also a moderate pain point, because it cost about 10% of my processing time budget. I could however, justify it by citing the specific regulatory text regarding fines for breach, and that I think made a huge difference in my ability to argue for that kind of security (this was perhaps, the hardest part).

I expect to be reading a wakeup call, we'll see how my prediction turns out.  Remember too, APIs are still a work in progress.

Security is hard.
Good Security is harder.
Great Security is an ongoing work in progress.
Perfect security does not exist.

Friday, August 20, 2021

Stratifying Race and Ethnicity for SANER

Variation is the bane of standards.  Eliminating needless variation is part of my job.  Doing it in a way that doesn't increase provider (or developer) burden is an indication that it's been done right.

I've looked at a lot of state and national dashboards while working on the SANER Project, and one thing I notice is the variation in reporting for data with respect to race and ethnicity classifications (strata).  Often, when reported on publicly, these two different categories are combined into smaller sets, with groupings like multiple race, other and unknown.

ONC National Coordinator Micky Tripathi noted Health IT reporting variation for this kind of data in his keynote delivered at a recent Strategic Health Information Exchange Collaborative (SHIEC) conference.

Federal reporting uses separate fields for race and ethnicity, and allow for multiple values to be reported for race.  There are 5 possible values for race (not counting various flavors of unknown), and two values for ethnicity according to OMB Reporting requirements.

Reporting multiple races means that there are several ways to report none (flavors of null including unknown, refused to answer and did not ask), 5 ways to report one race, 10 to report two races, 10 ways report three races, 5 ways to report four, and 1 way to report all five, resulting in around than 33 categories.  

Combining that with the various ways to report ethnicity (again with flavors of none), that results in about 165 possible reporting categories.  Looking at the actual statistics, there are about 50 categories that would generally be needed for a given facility (e.g., frequency > a few tenths of a percent) to stratify populations according to race and ethnicity, if non-existing groupings are not reported on, and perhaps an even smaller number for smaller facilities.  It wouldn't be possible for example, for a 100 bed hospital to even use all of the category combinations.

The data is generally rolled up into a much smaller number of reporting categories which vary between states, and these often also vary with how federal dashboards report the same data.  Different states have different racial and ethnic makeups and the public reporting at race and ethnicity data at these levels is designed to address potential disparities relevant to the state.

Given that many state departments of health  also support reporting to federal agencies, how does one normalize reporting without having to have 51 separate specifications for reporting?

The best way to handle this is to stratify by the combination of race and ethnicity, and report all possible existing combinations.  In other words, don't report 0 values for combinations that don't exist, as that can be inferred from the data.  This enables states to roll up this data into a smaller set of categories for their public reporting, yet retain the data needed for federal reporting, and enable federal reporting to roll up differently.  When automatically computed, this level of stratification does not introduce a reporting burden on the reporting providers.



Tuesday, August 3, 2021

Thinking Ahead

A very long time ago, when I worked at Florida State University, I had two rules for programming written on my board:

  1. Just get it to work.
  2. If it works, don't mess with it.
It became a test of the quality of people who would read it, as the ones who suffered from some sort of reaction while interpreting the ramifications were definitely the people that I wanted to have around me.

If you just get it to work, and don't mess with it, you have something that has the very least effort put into it.  If you need to change it in the future, good luck with that.

Along the same lines, when I was growing up, a kid who lived on my street had a small car, I think it was a Dodge of some sort, and he wanted to beef it up. So he took out the engine and tranny, and replaced it with a MUCH bigger one.  Somehow he managed to make it all fit together after modifying the drive shaft, but he forgot one very important thing: Engine torque.  When he finally started the car after spending the better part of a year on this project, he wound up warping the frame, something like the picture to the right.  The car was a total loss.  The point of this story is that you can only do so much with a limited infrastructure.

One of the challenges with FHIR adoption for some use cases is that there are existing HL7 interfaces, labs, ADT feeds, immunizations, et cetera, that are already widely deployed, adopted, and working, in fact, working well.  There's little desire to replace these interfaces with FHIR based interfaces because:
  1. What we have is good enough for what we are doing.
  2. It's working right now.
But as we keep pushing the interoperability needle higher and higher, eventually we will have replace these interfaces. When should we do that?
  1. When what we have isn't good enough for what we want to do.
  2. Or we can't make want we want to do easily work with what we have right now.
The HL7 V2 to FHIR project is an example of what happens when interfaces get stuck in these situations, we cannot easily connect them to newer infrastructure so that we can do more with them, so we build things that enable us to convert from one to the other.  The very existence of the project demonstrates that there's more than we want to be able to with the data present in HL7 Version 2 messages.  This might include things like:
  • Aggregating data from multiple sources
  • Providing more sophisticated searching capabilities
  • Enabling data subscriptions
There's a lot of effort and cost associated with replacing something that works with something else, and it's hard to justify that when the thing that's working is in fact still working.  But, if there was a way to upgrade, replacing your scooter with a Corvette (and you can justify the need for a Corvette), then it might in fact be worthwhile.

When interface standards are mandated by regulatory policy, it's pretty difficult to upgrade.  Consider what happened with X12 5010 standards, or the whole discussion around CCDA 1.1 and CCDA 2.1 backwards compatibility.  It's even more difficult when it all has to happen in a very short time frame.  We need to consider how to have policy enable these kinds of shifts, over REASONABLE time frames.  Two years is not enough time to roll out a new standard without severely impacting an industries capacity to do anything else but roll that out.  We know that from experience (or at least I hope we do).

But what would the next generation ADT, lab, immunization or other standards look like?  And what would they enable us to do that the current ones don't.  It's time to start thinking about that.


Monday, August 2, 2021

YAML as a FHIR Format

 

YAML (short for "YAML Ain't Markup Language" but I simply prefer "Yet Another Markup Language") is a file format that even further simplifies writing structured data files.  A while back I struggled with writing measures for the SANER Project because both XML and JSON formats have some minor issues that make it hard to hand code the expressions.

XML sucks because, well, XML sucks.  Whitespace is really valuable for formatting code, but XML just wants to make it to be gone.

JSON isn't much better because you have to escape newlines and once again, cannot see your code structure for expressions.

I dinked around a bit with YAML input and output, and now because I'm creating a new measure, wanted to get it working properly, which I have now done, at least in so far as my YAMLParser now correctly round trips from JSON to YAML and back to JSON.

The key to making this work is using Jackson to convert between JSON and YAML, and configuring the YAML quotes right so that strings that look like numbers (e.g., "001") don't get treated incorrectly as numbers when converting between the two.

The methods newYAMLMapper() creates a Jackson YAMLMapper correctly configured.

    public static YAMLMapper newYAMLMapper() {
        YAMLMapper m = new YAMLMapper();
        return
          m.enable(YAMLGenerator.Feature.LITERAL_BLOCK_STYLE)
           .disable(YAMLGenerator.Feature.MINIMIZE_QUOTES)
           .disable(YAMLGenerator.Feature.USE_PLATFORM_LINE_BREAKS)
           .disable(YAMLGenerator.Feature.SPLIT_LINES);
    }

Methods for converting between YAML and JSON are fairly simple:

    public static String fromYaml(String yaml) 
      throws JsonMappingException, JsonProcessingException
    {
        ObjectMapper yamlReader = newYAMLMapper();
        Object obj = yamlReader.readValue(yaml, Object.class);

        ObjectMapper jsonWriter = new ObjectMapper();
        return jsonWriter.writeValueAsString(obj);
    }
    public static String toYaml(String jsonString) throws IOException {
        // parse JSON
        JsonNode jsonNodeTree = new ObjectMapper().readTree(jsonString);
        // save it as YAML
        String jsonAsYaml = newYAMLMapper().writeValueAsString(jsonNodeTree);
        return jsonAsYaml;
    }

Converting from streams and readers works similarly.

The YamlParser class implements IParser.  It contains an embedded jsonParser convert resources back and forth between Java classes and JSON formats, and then uses toYaml and fromYaml methods in encodeResourceToString and parseResource methods to read/write in YAML format.  It's NOT the most efficient way to read/write YAML to FHIR, but it works (correctly as best I can tell).

    public static class YamlParser implements IParser {
        private final IParser jsonParser;
        YamlParser(FhirContext context) {
            jsonParser = context.newJsonParser();
        }

        @Override
        public String encodeResourceToString(IBaseResource theResource) 
            throws DataFormatException
        {
            try {
                return toYaml(jsonParser.encodeResourceToString(theResource));
            } catch (IOException e) {
                throw new DataFormatException("Error Converting to YAML", e);
            }
        }
        @Override
        public <T extends IBaseResource> T 
            parseResource(Class<T> theResourceType, InputStream theInputStream)
            throws DataFormatException {
            try {
                return jsonParser.parseResource(
                    theResourceType, fromYaml(theInputStream));
            } catch (IOException e) {
                throw new DataFormatException("Error Converting from YAML", e);
            }
        }
        ...
    }

All of the setter/getter methods on YamlParser delegate the work to the embedded JsonParser, as shown in the examples below.  

        @Override
        public void setEncodeElementsAppliesToChildResourcesOnly(
            boolean theEncodeElementsAppliesToChildResourcesOnly) {
            jsonParser.setEncodeElementsAppliesToChildResourcesOnly(
                theEncodeElementsAppliesToChildResourcesOnly);
        }

        @Override
        public boolean isEncodeElementsAppliesToChildResourcesOnly() {
            return jsonParser.isEncodeElementsAppliesToChildResourcesOnly();
        }


A full blown implementation can be found at YAML Utilities for FHIR 

Wednesday, June 16, 2021

Creating your FHIR Artifact JIRA Specification

When you go to ballot, or any form of publication for a FHIR IG through HL7, you have to provide an XML document that defines the pages and artifacts that the HL7 Jira system will use for reporting issues or comments on the specification.

If you are smart, you create an initial template in the HL7/JIRA-spec-artifacts page when you start your project.  If you aren't, you wait a while and then create it.

The initial version should look something like this:

<?xml version="1.0" encoding="UTF-8"?>
<specification
  gitUrl="https://github.com/HL7/fhir-project-mhealth"
  url="https://hl7.org/fhir/uv/mhealth-framework"
  ciUrl="http://build.fhir.org/ig/HL7/fhir-project-mhealth"
  defaultWorkgroup="mh" defaultVersion="0.1"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:noNamespaceSchemaLocation="../schemas/specification.xsd">
  <version code="0.1"/>
  <artifactPageExtension value="-definitions"/>
  <artifactPageExtension value="-examples"/>
  <artifactPageExtension value="-mappings"/>
  <page name="(NA)" key="NA"/>
  <page name="(many)" key="many"/>
  <page name="(profiles)" key="profiles"/>
</specification>

But you will need to add lines for each artifact (Profile, ValueSet, et cetera) and page (.md or .html file) that you generate.

  <artifact name="Artifact Title" 
    key="artifact-id" id="artifact-filename-without-extensions"/>
  <page name="Table of Contents" key="toc"/>

This can  be awfully tedious, but the IG Publisher can create an updated one for you, although if  you haven't created one in JIRA-spec-artificact, for some reason it doesn't seem to create an initial one for you.  I'm not clear on why it doesn't, but I found a workaround.

What you do is create that initial version, and copy it to your templates folder, naming it jiraspec.xml.  Then you run the IG Builder without a vocabulary server.
C:\myproject>  JAVA -jar "..\%publisher_jar%" -ig ig.ini -tx n/a

Telling the IG Builder that you don't have a vocabular server makes it assume that you do NOT have an internet connection, and so it also doesn't try to get your templates or copy the current JIRA Spec over that file I had you create above.  Now when you run the IG Builder, it will create an initial JIRA spec file for you, which you can then generate a pull request to https://github.com/HL7/JIRA-Spec-Artifacts/xml.

Once you've finished, you can find the created specification in your project in template/jira.xml, which you can then rename appropriately and send a pull request to the JIRA-spec-artifacts page.

Ideally, this specification configuration could be more automated in a FHIR IG Build, but for now what we have works.  It's just a bit of a pain.


Tuesday, October 27, 2020

Models of Clinical Decision Support

This is mostly a thinking out loud piece for me to wrap my head around some thoughts related to the work that I've done on Clinical Decision Support, and how that relates to work done recently for the SANER Project, and how to connect that to ECR, and other public health reporting efforts. My first step in this journey is to review what I've already written to see how my approach to CDS, and that of standards has evolved over time.

Some of the more interesting articles I've written on this topic include:

Most relevant to this discussion is the three legged stool of instance data, world knowledge, and computational algorithms from my first article.
The biggest difference in most implementations of clinical decision support is in where the algorithm gets executed, and quite a bit of effort has been expended in this arena.  I originally described this by referencing the "curly brace" problem of ARDEN Syntax, and it describes a challenge of integrating the algorithm for computing a response with a way of accessing instance data.

Here are the key principles:
  1. Separate data collection from computation. (Instance Data from Algorithms)
  2. Use declarative forms that can be turned into efficient computation (Algorithms).
  3. Separate inputs from outputs (Instance Data, Actions and Alerts).
The tricky bit, for which I don't HAVE a principle is in how to identify the essential instance data, which honestly is largely driven by domain knowledge, and this is where MUCH of the nuance about implementing clinical decision support comes into play.  

There are two main approaches to clinical decision support: Integrating it "inside" an information system that has access to the essential data, or moving the data to an information system that can efficiently compute a result.

The former operates on the assumption that if you have efficient access to data, then compute locally (where the data results), and you can thus skip the need to separate instance data from algorithms that implement knowledge.  The latter requires the separation of instance data from the algorithm to facilitate data movement.

A large distinction between what SANER and Clinical Quality Measurement does from the rest of Clinical Decision Support is largely based on the distinctions between the needs for systems supported decision support based upon population data (data in bulk), and systems making decisions at the level of an individual.

It largely boils down to a question of how to access data efficiently. Different approaches to clinical decision support each approach this in a slightly different way.
  • Quality Reporting Data Architecture (QRDA) defines a format to move data needed for quality measurement to a service that can evaluate measures.
  • Query Health used Health Quality Measure Format (HQMF) to move a query described in declarative to a data source for local execution, and then results back to a service that can aggregate them across multiple sources.
  • HQMF itself has evolved from an HL7 Version 3 declarative form to one that is now largely based on the Clinical Quality Language (CQL) which is also a declarative language (and a lot easier to read).
  • Electronic Case Reporting (eCR) uses a trigger condition defined using the Reportable Condition Mapping Table (RCMT) value set to move a defined collection of data (as described in eICR) from a data source to the Reportable Condition Knowledge Management Service (RCKMS) which can provide a reportability response including alerts, actions and information.  RCKMS is a clinical decision support service.
  • CDS Hooks defines hooks that can be triggered by an EHR to move prefetch data to a decision support service using SMART on FHIR, which can then report back alerts, actions and other information as FHIR Resources.
  • SANER defines an example measure in a form which is represented by an initial query, and then filtering of that, using FHIRPath, which may result in subsequent queries and filtering.
One of the patterns that appears in many CDS specifications is about optimization of data flow.  There's an initial signal executed locally, which is used to selectively identify the need for CDS computation.  That signal is represented by a trigger event or condition, driven by either workflow, or a combination of workflow and instance data.  One example of a trigger event is the creation of a resource (row, database record, chart entry, FHIR resource, et cetera) matching a coded criterion (e.g., as in RCMT used with RCKMS for ECR). 

The trigger/collect/compute pattern is pervasive not just in clinical decision support, but in other complex (non-clinical) decision support problems that deal with complex domain knowledge.  It has uses in natural language processing software, where it has been used for grammar correction, e.g., to detect a linguistic pattern, evaluate it against domain (and language) specific rules, and then suggest alternatives (or verify correctness).  The goal of this approach is multi-fold: optimization of integration and data flow, and separation of CDS logic (and management thereof) from system implementation.

Population based clinical decision support is often expensive because it may require evaluation of thousands (or hundreds of thousands) of data records, and the more than can be done to reduce the number of records that need to be moved, the more efficiently and quickly such evaluations can be performed.  FHIR Bulk Data Access (a.k.a., Flat FHIR) is an approach to moving large quantities of data to support population health management activities.  It further accentuates the need for optimization of data movement to support population management.

As I think again through all of what has gone before, one of the things missing from my three legged model is the notion of "triggers", and I think these deserve further exploration.  What is a trigger event?  In standards this is nominally a workflow state.  From a CDS perspective, it's the combination of a workflow state, associated with resource matching a specific criteria.  The criteria is generally pretty straightfoward: This kind of thing, with that kind of value, having a measurement in this range, in this time frame.  And in fact, the workflow state is almost irrelevant -- but is usually essential for determining the best time to evaluate a trigger event.  Consider ECR for example, you probably don't want to trigger a reportability request until after the clinician has entered all essential data that you might want to compute with, at the same time, you don't want to wait until after the visit is over to provide a response.  Commonly this sort of thing might be triggered "prior to signing the chart", given that you want to make sure that the data is complete.  However, given that the results may influence the course of treatment or management, a more ideal time might be just before creation of the plan of care for the patient.

A few years back I worked on a project demonstrating the use of "Public Health Alerts" using the Infobutton profile and a web services created by John's Hopkins APL that integrated with an EHR system that was developed by my then employer.  We actually used two different trigger events, the first one being after "Reason for Visit" was known, and the second one just before physical exam, after all symptoms and vital signs had been recorded (if I remember correctly).  This was helpful, b/c the first query was relatively thin on data, but could guide data collection efforts if there was a positive hit, and the second one could pick up with a better data set to capture anything that the first might have missed.

I'm not done thinking all this through, but at least I've got a first start, I'm sure to write more on this later.

Monday, October 5, 2020

HL7 FHIR SANER Ballot Signup Closing October 19

I sent the following e-mail out to a subset of the SANER IG distribution list we maintain internally for folks who have been involved in development of The SANER Project.  I didn't bother to send to those who work for organizations had already signed up to participate in the ballot.  For those of you who have been following from afar, this is an opportunity for you to look more closely at what we've been doing for the past 8 months, and contribute your input!


     Keith

As someone who has expressed interest, or having participated in the development of the HL7 FHIR Situation Awareness for Novel Epidemic Response Implementation Guides, we are letting you know that this document will soon be published for ballot. 

You will need to sign up BEFORE October 19th, 2020 to be included in the voting pool should you have interest in voting on this implementation guide in the next ballot cycle

To sign up to participate, go to http://www.hl7.org/ctl.cfm?action=ballots.home.  If you are an HL7 Voting Member for your organization, you will need to log in to see the ballots that you can vote on.  

If you are not a member, you can participate in an HL7 Ballot pool by paying creating an HL7 Profile and paying applicable administration fees (See http://www.hl7.org/documentcenter/public/ballots/2021JAN/Announcements/NonMember%20Participation%20in%20HL7%20Ballots%20Instructions.pdf for details).

Thank you all for your contributions, we have accomplished a tremendous amount of work over the last 8 months, and we hope to see you comments on this implementation guide.  Feel free to pass this information along to others you think should participate in voting on this implementation guide.

Keith W. Boone


Tuesday, September 22, 2020

A Test Case Generator for FHIR and SUSHI (and SANER )

I've often heard the complaint of combinatorial explosion with respect to creating test cases to fully test a system.  The problem is acute.  One part of the solution is good analysis, but the other part of it is automation.

It must be my week for mini-languages, because here is another example of a mini-language, this time used for test case generation.  I think I might have caught the language virus.

TestCase Case1:
    Patient patient X 30 with (
        identifier.value = Identifier,
        identifier.system = "http://sanerproject.org/testdata/patients",
        name.given in "firstnames",
        name.family in "lastnames",
        gender in "genders",
        birthDate within '@1930-09-09' to '@2020-09-09'
    )
Values
    // This is a set of common last names, it is purposefully of prime length
    "lastnames": {
        Smith
        Johnson
        Williams
        Brown
        Jones
        Garcia
        Miller
        Davis
        Rodriguez
        Martinez
        Hernandez
        Lopez
        Gonzalez
        Wilson
        Anderson
        Thomas
        Taylor
        Moore
        Jackson
        Martin
        Lee
        Perez
        Thompson
    }
    // This is a set of first names that are gender free, also of prime length
    // and mutually prime with the set of last names.
    "firstnames": {
        Alex
        James
        Blake
        Kyle
        Drew
        Taylor
        Kennedy
        Jordan
        Parker
        Avery
        Ryan
        Brooklyn
        Cameron
        Logan
        Emerson
        Charlie
        Ezra
    }
    "genders": {
        male
        female
    }

This example says: Generate a test case (in a bundle) containing the resource "Patient" with identifier "patient" and do it 30 times.  Take the identifier.value from an autoincrementing counter.  Set the identifier.system to a fixed value.  Pull given and family names from predefined list of values, iterating over them until done.  Take gender from another list with only two codes.  Generate birth dates from a range of values.

Now, if that was all my language did, you'd not be terribly impressed (or at least I wouldn't be).

But, what if, you could generate multiple resources, and link them correctly by identifiers.  Now we are starting to get somewhere, but it still isn't all that much better than what one can already do with an excel spreadsheet (as we did manually for the first set of test data for SANER automation).

But I also need test cases where I have encounters with, and without reasonReference as variations, with with and without reasonCode values matching a certain value set, and observation and condition resources that match or don't match selection criteria.

So, what if I could specify variation within a field like this:
Patient patient1 
    /* as before */

Condition condition1 with (
   code in COVID19Diagnosis OR in NotACovid19Diagnosis,
   patient = patient1
)

Encounter encounter1 with (
   reasonReference = condition1 OR missing,
   reasonCode in COVID19Diagnsoses OR missing,
   subject = patient1
)

And what if the test case generator spit out were six different bundles where each bundle contained a patient1, condition1 and encounter1 meeting all the appropriate mixes of criteria.

Bundle1: 
condition1 with COVID19Diagnosis, 
encounter1 with reasonReference to condition1 and reasonCode in COVID19Diagnosis
Bundle2:
condition1 with COVID19Diagnosis,
encounter1 with reasonReference to condition1 and reasonCode missing
Bundle3:
condition1 with COVID19Diagnosis
encounter1 with reasonRererence missing and reasonCode in COVID19Diagnosis
Bundle4:
condition1 with NotACovid19Diagnosis
encounter1 with reasonReference to condition1 and reasonCode in COVID19Diagnosis
Bundle5:
condition1 with NotACovid19Diagnosis
encounter1 with reasonReference to condition1 and reasonCode missing
Bundle6:
condition1 with NotACovid19Diagnosis
encounter1 with reasonReference to condition1 and reasonCode in COVID19Diagnosis

OK, now we are talking about something useful.
And if patient1 can vary (non-essentially) across these encounters, and so can encounter location, we've gone a step better.

Yes, this could produce a muck-ton of data.  But, the computer did it, you didn't have to.  All you had to do was give it the correct instructions to do something useful, and it produced something that you can use.

Anyway, more on this later as I continue my experiments.   So far, this was about two days of work, and what I have to show for it is this sample output:

Instance: patient3
InstanceOf: Patient
Description: "Generate sample patients with random characteristics"
* birthDate = "1964-06-09"
* extension[0].extension[0].url = "ombCategory"
* extension[0].extension[0].valueCoding = urn:oid:2.16.840.1.113883.6.238#2054-5 "Black or African American"
* extension[0].url = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race"
* extension[1].extension[0].url = "ombCategory"
* extension[1].url = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity"
* gender = #male
* identifier.system = "http://sanerproject.org/testdata/patients"
* identifier.value = "3"
* name.family = "Williams"
* name.given = "Drew"
* name.given[1] = "Taylor"
Instance: patient4
InstanceOf: Patient
Description: "Generate sample patients with random characteristics"
* birthDate = "1975-09-09"
* extension[0].extension[0].url = "ombCategory"
* extension[0].extension[0].valueCoding = urn:oid:2.16.840.1.113883.6.238#2076-8 "Native Hawaiian or Other Pacific Islander"
* extension[0].url = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race"
* extension[1].extension[0].url = "ombCategory"
* extension[1].extension[0].valueCoding = urn:oid:2.16.840.1.113883.6.238#2135-2 "Hispanic or Latino"
* extension[1].url = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity"
* gender = #female
* identifier.system = "http://sanerproject.org/testdata/patients"
* identifier.value = "4"
* name.family = "Brown"
* name.given = "Kennedy"
* name.given[1] = "Jordan"
Instance: patient5
InstanceOf: Patient
Description: "Generate sample patients with random characteristics"
* birthDate = "1986-12-09"
* extension[0].extension[0].url = "ombCategory"
* extension[0].extension[0].valueCoding = urn:oid:2.16.840.1.113883.6.238#2106-3 "White"
* extension[0].url = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race"
* extension[1].extension[0].url = "ombCategory"
* extension[1].extension[0].valueCoding = urn:oid:2.16.840.1.113883.6.238#2186-5 "Non Hispanic or Latino"
* extension[1].url = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity"
* gender = #male
* identifier.system = "http://sanerproject.org/testdata/patients"
* identifier.value = "5"
* name.family = "Jones"
* name.given = "Parker"
* name.given[1] = "Avery"
Instance: patient6
InstanceOf: Patient
Description: "Generate sample patients with random characteristics"
* birthDate = "1998-03-10"
* extension[0].extension[0].url = "ombCategory"
* extension[0].extension[0].valueCoding = http://terminology.hl7.org/CodeSystem/v3-NullFlavor#UNK "Unknown"
* extension[0].url = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race"
* extension[1].extension[0].url = "ombCategory"
* extension[1].url = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity"
* gender = #female
* identifier.system = "http://sanerproject.org/testdata/patients"
* identifier.value = "6"
* name.family = "Garcia"
* name.given = "Ryan"
* name.given[1] = "Brooklyn"
Instance: patient7
InstanceOf: Patient
Description: "Generate sample patients with random characteristics"
* birthDate = "2009-06-09"
* extension[0].extension[0].url = "ombCategory"
* extension[0].extension[0].valueCoding = http://terminology.hl7.org/CodeSystem/v3-NullFlavor#ASKU "Asked but no answer"
* extension[0].url = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race"
* extension[1].extension[0].url = "ombCategory"
* extension[1].extension[0].valueCoding = urn:oid:2.16.840.1.113883.6.238#2135-2 "Hispanic or Latino"
* extension[1].url = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity"
* gender = #male
* identifier.system = "http://sanerproject.org/testdata/patients"
* identifier.value = "7"
* name.family = "Miller"
* name.given = "Cameron"
* name.given[1] = "Logan"