FHIRPath notes

Russell Bateman
February 2020
last update:





FHIRPath helper

I sometimes find FHIRPath very challenging. For a long time, I had a JUnit test class that allowed me to dummy up FHIRPaths to try out against FHIR resources. Then a colleague pointed out this helper on GitHub:

HL7 / fhirpath.js

You have to paste in a JSON or YAML (FHIR) resource into the big window (it doesn't do XML which I read more easily). Then you can try out different FHIRPath "queries" against it. For example, I took the patient that was already there, then I tried out this FHIRPath:

Patient.name.where( use = 'official' ).given.first() + ' ' + Patient.name.given[ 1 ] + ' ' + Patient.name.family[ 0 ]

That netted me the response:

- Peter James Chalmers

Hate JSON and prefer XML?

I read JSON, but FHIR documents are so big and I learned FHIR many years ago when (at least I thought) everyone was doing XML at the time. So, here's a translator:

Your FHIR® in XML or JSON

Mind that you don't go translating anything PHI on-line.


Time out for FHIRPath...

Of course, we know how to parse patient data out of a FHIR document:

  private static final String PATIENT = ""
      +  "<Patient xmlns=\"http://hl7.org/fhir\">\n"
      +  "  <id value=\"9.PI\" />\n"
      +  "  <meta>\n"
      +  "    <lastUpdated value=\"2016-04-22T00:00:00.000-06:00\" />\n"
      +  "  </meta>\n"
      +  "  <name>\n"
      +  "    <family value=\"Munster\" />\n"
      +  "    <given value=\"Herman\" />\n"
      +  "  </name>\n"
      +  "  <gender value=\"male\" />\n"
      +  "  <birthDate value=\"1835-10-31\" />\n"
      +  "  <address>\n"
      +  "    <text value=\"1313 Mockingbird Lane, Mockingbird Heights, Beverly Hills, CA 90210\" />\n"
      +  "    <line value=\"1313 Mockingbird Lane\" />\n"
      +  "    <city value=\"Beverly Hills\" />\n"
      +  "    <state value=\"CA\"/>\n"
      +  "    <postalCode value=\"90210\" />\n"
      +  "  </address>\n"
      +  "</Patient>";

First, straightforwardly using HAPI FHIR

  @Test
  public void testStraight()
  {
    long start = System.currentTimeMillis();

    final FhirContext  context    = FhirContext.forR4();
    final IParser      parser     = context.newXmlParser();
    final Patient      patient    = parser.parseResource( Patient.class, new ByteArrayInputStream( PATIENT.getBytes() ) );

    for( HumanName name : patient.getName() )
      System.out.println( name.getFamily() );

    for( HumanName name : patient.getName() )
      System.out.println( name.getFamily() + ", " + name.getGivenAsSingleString() );

    System.out.println( patient.getBirthDate().toString() );

    System.out.println( patient.getAddress().get( 0 ).getPostalCode() );

    System.out.println( patient.getId() );

    long end = System.currentTimeMillis();
    System.out.println( "Time to run: " + ( end - start ) + " milliseconds." );
  }

  /* output:
    Munster
    Munster, Herman
    Sat Oct 31 00:00:00 MST 1835
    90210
    Time to run: 38 milliseconds.
   */

However!

...it's also possible to do it using FHIRPath


  @Test
  public void testUsingFHIRPath()
  {
    long start = System.currentTimeMillis();

    final FhirContext  context    = FhirContext.forR4();
    final IParser      parser     = context.newXmlParser();
    final Patient      patient    = parser.parseResource( Patient.class, new ByteArrayInputStream( PATIENT.getBytes() ) );

    final String       familyPath = "Patient.name.family";
    List< StringType > family     = context.newFhirPath().evaluate( patient, familyPath, StringType.class );
    for( StringType f : family )
      System.out.println( f );

    final String       namePath = "Patient.name";
    List< HumanName >  name     = context.newFhirPath().evaluate( patient, namePath, HumanName.class );
    for( HumanName n : name )
      System.out.println( n.getFamily() + ", " + n.getGivenAsSingleString() );

    final String       dobPath = "Patient.birthDate.value";
    List< StringType > dob     = context.newFhirPath().evaluate( patient, dobPath, StringType.class );
    for( StringType d : dob )
      System.out.println( d );

    final String       zipPath = "Patient.address.postalCode";
    List< StringType > zip     = context.newFhirPath().evaluate( patient, zipPath, StringType.class );
    for( StringType z : zip )
      System.out.println( z );

    final String           idPath = "Patient.id.value";
    Optional< StringType > id     = context.newFhirPath().evaluateFirst( patient, idPath, StringType.class );
    //noinspection OptionalGetWithoutIsPresent
    System.out.println( id.get() );

    long end = System.currentTimeMillis();
    System.out.println( "Time to run: " + ( end - start ) + " milliseconds." );
  }

  /* output:
    Munster
    Munster, Herman
    1835-10-31
    90210
    Time to run: 545 milliseconds.
   */

Notes on   evaluate( IBase, String, Class ):

  1. the first argument (lines 11, 16, 21, 26, 31) is the parsed FHIR content (from line 8).
  2. the second argument is the FHIR path we want (lines 10, 15, 20, 25, 30). Note, however, that there are FHIR paths potentially way more complex than these.
  3. the third argument is the type of the response to be returned. It could simply be IBase.class, the ancestor of StringType, but I made it more explicit. Whatever the case, it must match the type of the variable about to collect it.
  4.  
  5. evaluate() returns a list of everything that matches the second argument. (As illustrated in our examples, the list may be only 1 deep, but this is only the case because of my pawltry Patient.)
  6. evaluateFirst() returns the first thing that matches the second argument. As noted, in our examples, there was only one thing that matched, so the result is the same. I used this nevertheless in the last example for completeness.
  7. I don't have much of a benchmark, but what I do have plus the lag-time in the debugger tells me that using FHIRPath is a very expensive way to tear apart a FHIR document. It's much faster going to use the direct approach (14×) if you're doing lots of parsing while HAPI FHIR is holding it in its hand.
  8.  
  9. The reason for HAPI FHIR FHIRPath rather than simply XPath is because FHIR can be expressed several formats, like JSON, and not just XML. And FHIR (XML, at least) has an order to its elements.

Here are the imports for the two tests above:

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.List;
import java.util.Optional;

import org.junit.Test;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
import ca.uhn.fhir.parser.IParser;

import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext;
import org.hl7.fhir.r4.model.Base;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Encounter;
import org.hl7.fhir.r4.model.HumanName;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.utils.FHIRPathEngine;

import com.windofkeltia.utilities.TestUtilities;

The FHIRPath cookie jar—everything you need!

Here's an exhaustive sample in Java to find a FHIR Patient resource in a FHIR document (whose contents have already been read into variable CONTENTS) using FHIRPath. This includes priming the pump for the HAPI FHIR parser, reading in an parsing the FHIR document (usually a Bundle), priming the pump for the FHIRPath component and extracting a Patient resource from the FHIR document using a FHIRPath.

// priming the HAPI FHIR parser:
FhirContext                     context    = FhirContext.forR5();
IParser                         parser     = context.newXmlParser();

// reading the FHIR document into a resource:
BaseResource                    bundle     = parser.parseResource( CONTENT.getBytes() );

// priming the pump for the FHIRPath component:
DefaultProfileValidationSupport validation = new DefaultProfileValidationSupport( context );
IWorkerContext                  worker     = new HapiWorkerContext( context, validation );
FHIRPathEngine                  fhirEngine = new FHIRPathEngine( worker );

// extracting a Patient resource from the FHIR document using FHIRPath:
List< Base >                    patients   = fhirEngine.evaluate( bundle, "Bundle.entry.resource.ofType(Patient)" ); *
Patient                         patient    = ( Patient ) patients.get( 0 );

For quick copy and paste:

FhirContext                     context    = FhirContext.forR5();
IParser                         parser     = context.newXmlParser();
BaseResource                    bundle     = parser.parseResource( CONTENT.getBytes() );
DefaultProfileValidationSupport validation = new DefaultProfileValidationSupport( context );
IWorkerContext                  worker     = new HapiWorkerContext( context, validation );
FHIRPathEngine                  fhirEngine = new FHIRPathEngine( worker );
List< Base >                    patients   = fhirEngine.evaluate( bundle, "Bundle.entry.resource.ofType(Patient)" ); *
Patient                         patient    = ( Patient ) patients.get( 0 );

Extra credit

Once you've got a Patient, such as produced above, you can isolate a particular Identifier (for the reason of this, see Yet More FHIRPath...).


...

// extracting a specific Identifier resource from the Patient using FHIRPath:
List< Base > identifiers = fhirEngine.evaluate( patient, "Patient.identifier.where(extension('http://acme.us/fhir/extensions/mpi'))" );
Identifier   identifier  = identifiers.get( 0 );

The "cost" of FHIRPath

Previously, you learned that this code will get you a patient out of a bundle:

FhirContext                     context    = FhirContext.forR5();
IParser                         parser     = context.newXmlParser();
BaseResource                    bundle     = parser.parseResource( CONTENT.getBytes() );
DefaultProfileValidationSupport validation = new DefaultProfileValidationSupport( context );
IWorkerContext                  worker     = new HapiWorkerContext( context, validation );
FHIRPathEngine                  engine     = new FHIRPathEngine( worker );
List< Base >                    patients   = fhirEngine.evaluate( bundle, "Bundle.entry.resource.ofType(Patient)" ); *
Patient                         patient    = ( Patient ) patients.get( 0 );

However, you must understand the cost to execute. The highlighted statements here cost, in my present development environment, 2 seconds or a bit more, while the elapsed time to execute the other statements is near 0.

Thus, you'll want to isolate your initialzation of the FHIRPath engine to a run-once or initialization segment of your application:

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
import org.hl7.fhir.r5.hapi.ctx.HapiWorkerContext;
import org.hl7.fhir.r5.utils.FHIRPathEngine;
.
.
.
final FhirContext                     context    = FhirContext.forR5();
final DefaultProfileValidationSupport validation = new DefaultProfileValidationSupport( context );
final IWorkerContext                  worker     = new HapiWorkerContext( context, validation );
final FHIRPathEngine                  engine     = new FHIRPathEngine( worker );

More FHIRPath experimentation...

Experimentation: I do it the quick way, which is tantamount to writing HAPI FHIR code as if I know what I'm doing (well, okay, I do) then I resort to using FHIRPath three times, three different ways to achieve the same result. FHIRPath is going to be useful in the case where I "don't know what I'm doing," for example, the path is passed into me to look up in a resource (so I can't code sciently ahead).


Disclaimer: Since conquering the "cost" of using FHIRPath, I no longer do it the "quick way" since FHIRPath, once an engine is initialized, does not slow anything down.

The lines implementing a "direct," i.e.: sans FHIRPath, approach are highlighted here. The remaining lines employ the IFhirPath.evaluate() solution.

Imports to add to what's already being used are listed here.

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import org.hl7.fhir.r4.model.Medication;
import org.hl7.fhir.r4.model.MedicationDispense;
import org.hl7.fhir.r4.model.Resource;

  @Test
  public void testMedicationEmbeddedInMedicationDispense() throws IOException
  {
    final String              PATHNAME           = TestUtilities.TEST_RESOURCES + "flows/test-files/medicationdispense-3.xml";
    final String              CONTENT            = TestUtilities.getLinesInFile( PATHNAME );
    final FhirContext         context            = FhirContext.forR4();
    final IParser             parser             = context.newXmlParser();
    final MedicationDispense  medicationDispense = parser.parseResource( MedicationDispense.class,
                                                        new ByteArrayInputStream( CONTENT.getBytes() ) );
    List< Resource > resources  = medicationDispense.getContained(); assertFalse( resources.isEmpty() );
    Resource         resource   = resources.get( 0 );                assertTrue( resource instanceof Medication );
    Medication       medication = ( Medication ) resource;
    System.out.println( medication.fhirType() );

    final String           medicationPath  = "MedicationDispense.contained";
    Optional< Medication > m               = context.newFhirPath().evaluateFirst( medicationDispense, medicationPath, Medication.class );
    System.out.println( m.get().fhirType() );

    Optional< Resource >   r               = context.newFhirPath().evaluateFirst( medicationDispense, medicationPath, Resource.class );
    System.out.println( r.get().fhirType() );

    final String           medicationPath2 = "MedicationDispense.contained.ofType(Medication)";
    Optional< Medication > m2              = context.newFhirPath().evaluateFirst( medicationDispense, medicationPath2, Medication.class );
    System.out.println( m2.get().fhirType() );

    /* output
        Medication
        Medication
        Medication
        Medication
     */
  }

Yet more FHIRPath...

FHIRPath is pretty powerful. You can do all sorts of crazy things. Here's a way to pluck a value out of an identifier that's concocted (proprietarily) to hold something called a "master patient index" or MPID. (An example of this in XML is given at the end of this note.)

package com.windofkeltia.hapi.fhir;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.List;

import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
import ca.uhn.fhir.parser.IParser;
import org.hl7.fhir.r5.hapi.ctx.HapiWorkerContext;
import org.hl7.fhir.r5.model.Base;
import org.hl7.fhir.r5.model.BaseResource;
import org.hl7.fhir.r5.model.Encounter;
import org.hl7.fhir.r5.model.Extension;
import org.hl7.fhir.r5.model.Identifier;
import org.hl7.fhir.r5.model.Patient;
import org.hl7.fhir.r5.utils.FHIRPathEngine;

import com.windofkeltia.utilities.TestUtilities;

public class ExploitFhirPathTest
{
  @Rule   public TestName name = new TestName();
  @After  public void tearDown() { }
  @Before public void setUp()
  {
    TestUtilities.setUp( name );
    context    = FhirContext.forR5();
    parser     = context.newXmlParser().setPrettyPrint( true );
    fhirEngine = new FHIRPathEngine( new HapiWorkerContext( context, new DefaultProfileValidationSupport( context ) ) );
  }

  private static final boolean VERBOSE  = TestUtilities.verboseIfAttentive();

  private FhirContext    context;
  private IParser        parser;
  private FHIRPathEngine fhirEngine;

  @Test
  public void test() throws IOException
  {
    final int    LIMIT    = 100000;
    final String FHIRPATH = "Patient.identifier.where(extension('http://acme.us/fhir/extensions/mpi')).value";
    long         usingFhirPath, goingDirect;

    Patient patient = getLilyMunster();

    // measure FHIRPath elapsed time ------------------------------------------------------------------------------
    usingFhirPath = System.currentTimeMillis();

    for( int count = 0; count < LIMIT; count++ )
    {
      List< Base > values = fhirEngine.evaluate( patient, FHIRPATH );

      if( values.size() != 1 )
        fail( "No MPID found" );

      String mpid = String.valueOf( values.get( 0 ) );
    }

    usingFhirPath = System.currentTimeMillis() - usingFhirPath;

    // measure direct-code elapsed time ---------------------------------------------------------------------------
    goingDirect = System.currentTimeMillis();

    for( int count = 0; count < LIMIT; count++ )
    {
      List< Identifier > identifiers = patient.getIdentifier();

      Identifier mpidIdentifier = null;

      for( Identifier identifier : identifiers )
      {
        List< Extension > extensions = identifier.getExtension();

        for( Extension extension : extensions )
        {
          String url = extension.getUrl();

          if( url.equals( "http://acme.us/fhir/extensions/mpi" ) )
          {
            mpidIdentifier = identifier;
            break;
          }
        }

        if( nonNull( mpidIdentifier ) )
          break;
      }

      if( isNull( mpidIdentifier ) )
        fail( "Unable to find desired extension URL" );

      String mpid = mpidIdentifier.getValue();

      if( mpid.isEmpty() )
        fail( "No MPID found" );
    }

    goingDirect = System.currentTimeMillis() - goingDirect;

    if( VERBOSE )
    {
      System.out.println( "Benchmark, executing " + LIMIT + " iterations," );
      System.out.println( "         FHIRPath took " + usingFhirPath + " milliseconds" );
      System.out.print  ( "  direct approach took " + goingDirect + " millisecond" );
      System.out.println( ( goingDirect == 1 ) ? "" : "s" );
    }
  }

  /** "Lily Munster" because that's the Patient's name in our test fodder. */
  private Patient getLilyMunster() throws IOException
  {
    final String PATHNAME = TestUtilities.TEST_FODDER + "/lily-munster.xml";
    final String CONTENT  = TestUtilities.getLinesInFile( PATHNAME );
    BaseResource resource = ( BaseResource ) parser.parseResource( new ByteArrayInputStream( CONTENT.getBytes() ) );
    List< Base > patients = fhirEngine.evaluate( resource, "Bundle.entry.resource.ofType(Patient)" );

    if( patients.isEmpty() )
      fail( "Failed to find Patient Lily Munster" );

    return ( Patient ) patients.get( 0 );
  }
}

I got the following output at one point:

Test: testBenchmark ---------------------------------------------------------------------------------------
Benchmark, executing 100000 iterations,
         FHIRPath took 537 milliseconds
  direct approach took 7 milliseconds

As you can see, using FHIRPath, once you've set it up, is elegant, but there's certainly a substantial penalty.

Here's lily-munster.xml and the identifier that interests us in particular:

<Bundle xmlns="http://hl7.org/fhir">
  <entry>
    <resource>
      <Patient xmlns="http://hl7.org/fhir">
        <meta>
          <lastUpdated value="2021-01-05T00:00:00.000-06:00" />
        </meta>
        <id value="9812850.PI"/>
        <identifier>
          <extension url="http://acme.us/fhir/extensions/mpi">
            <extension url="PIRecord">
              <valueString value="9812850.PI"/>
            </extension>
          </extension>
          <system value="https://test.acme.io/mpi/0/" />
          <value value="665892"/>
          <assigner>
            <display value="Test Performance MPI system"/>
          </assigner>
        </identifier>
        <identifier>
          <system value="https://fhir.acme.io/facility/Beverly Hills Clinic" />
          <value value="3660665800" />
          <assigner>
            <display value="Beverly Hills Clinic" />
          </assigner>
        </identifier>
        <name>
          <family value="Munster" />
          <given value="Lily" />
        </name>
        <gender value="female" />
        <birthDate value="1827-04-01" />
        <deceasedBoolean value="false" />
        <address>
          <text value="1313 Mockingbird Lane, Mockingbird Heights, Beverly Hills, CA 90210" />
          <line value="1313 Mockingbird Lane" />
          <city value="Beverly Hills" />
          <state value="CA"/>
          <postalCode value="90210" />
        </address>
        <telecom>
          <system value="phone" />
          <value value="+3035551212" />
          <use value="home" />
        </telecom>
      </Patient>
    </resource>
  </entry>
  <entry>
    <fullUrl value="urn:uuid:f78d73fc-9f9b-46d5-93aa-f5db86ba914c" />
    <resource>
      <Encounter xmlns="http://hl7.org/fhir">
        <class>
          <system value="http://terminology.hl7.org/CodeSystem/v3-ActCode" />
          <code value="EMER" />
        </class>
        <type>
          <coding>
            <system value="http://snomed.info/sct" />
            <code value="410429000" />
            <display value="Cardiac Arrest" />
          </coding>
          <text value="Cardiac Arrest" />
        </type>
        <subject>
          <reference value="urn:uuid:5cbc121b-cd71-4428-b8b7-31e53eba8184" />
          <display value="Mrs. Lily Munster" />
        </subject>
        <participant>
          <individual>
            <reference value="urn:uuid:0000016d-3a85-4cca-0000-000000000122" />
            <display value="Victor von Frankenstein, M.D." />
          </individual>
        </participant>
        <period>
          <start value="1965-11-15T06:22:41-05:00" />
          <end value="1965-11-15T08:07:41-05:00" />
        </period>
        <serviceProvider>
          <reference value="urn:uuid:8ad64ecf-c817-3753-bee7-006a8e662e06" />
          <display value="Beverly Hills Hospital" />
        </serviceProvider>
      </Encounter>
    </resource>
    <request>
      <method value="POST" />
      <url value="Encounter" />
    </request>
  </entry>
</Bundle>

Still more FHIRPath illustrations: unclear datum types...

In an HL7v4 Observation and many other FHIR resources, there is a field, status whose type is often Observation.ObservationStatus, Claim.ClaimStatus, etc.

In most cases, when using a FHIRPath to reach a datum, you want to have to express the least amount of knowledge of the object (Observation, Account, etc.) possible. Yet, how to arrive at a datum documented thus? (channeling HL7FHIR Observation: Resource Content here)

status    1..1 code    registered | preliminary | final | amended +

You're tempted to give into this:

FhirContext context = FhirContext.forR4();
IParser     parser  = context.newXmlParser().setPrettyPrint( true );
Claim       claim   = parser.parseResource( Claim.class, "<Claim ..." );

if( !claim.hasStatus() )
  fail( "Test fodder needs a Claim." );

Object object = claim.getStatus();

if( object instanceof Claim.ClaimStatus )
{
  String status = ( ( Claim.ClaimStatus ) object ).getDisplay();
  System.out.println( "Status: " + status );
}

...but that would mean having knowledge about the object when you just have a FHIRPath and don't want to have to know that the datum at the end of the path is a Claim.ClaimStatus.

The generic solution for handling this case is as follows. Note that the only appearance of "Observation" is in the FHIRPath, not in the Java code:

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
import ca.uhn.fhir.parser.IParser;

import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext;
import org.hl7.fhir.r4.model.Base;
import org.hl7.fhir.r4.model.Enumeration;
import org.hl7.fhir.r4.utils.FHIRPathEngine;

@Test
public void test()
{
  final String FHIRPATH = "Bundle.entry.resource.ofType(Observation).status";

  FhirContext                     context     = FhirContext.forR4();
  IParser                         parser      = context.newXmlParser().setPrettyPrint( true );
  DefaultProfileValidationSupport validation  = new DefaultProfileValidationSupport( context );
  HapiWorkerContext               worker      = new HapiWorkerContext( context, validation );
  FHIRPathEngine                  engine      = new FHIRPathEngine( worker );
  Bundle                          bundle      = parser.parseResource( Bundle.class, BUNDLE );

  List< Base >                    values      = engine.evaluate( observation, FHIRPATH );

  if( values.size() != 1 )
    fail( "Unable to find " + FHIRPATH );

  Object object = values.get( 0 );

  System.out.println( "Object: " + object );

  if( object instanceof Enumeration< ? > )
    System.out.println( "Object is Enumeration< ? >" );

  System.out.println( "String: " + String.valueOf( object ) );

  Enumeration< ? > enumeration = ( Enumeration< ? > ) object;
  String           e           = enumeration.getValueAsString();

  System.out.println( "Enumeration string: " + e );
}

Output from test():

Object: Enumeration[final]
Object is Enumeration< ? >
String: Enumeration[final]
Enumeration string: final

The solution in the case of status, which is a code, is to exploit it as a generic Enumeration (of unknown type). I have left in verbose stabbing around in the code, which you can observe in the debugger, to show how to arrive at this conclusion because it's a principle that maps to other, similar type-difficult situations arising at the end of a FHIRPath.


FHIRPath end-point catch-all

Still, about 50% of datatypes or more at the end of any FHIRPath* can be resolved to strings thus:

  final String FHIRPATH = "Bundle.entry.resource.ofType(Observation).valueInteger";
  ...
  List< Base > values = engine.evaluate( observation, FHIRPATH );

  if( values.size() != 1 )
    fail( "Unable to find " + FHIRPATH );

  Object object = values.get( 0 );

  try
  {
    String value = values.get( 0 );
    System.out.println( "Significant value: " + value );
    return value;
  }
  catch( NullPointerException e )
  {
    if( nonNull( logger ) )
      logger.debug( "Extractor unable to harvest string from data type pointed at by the end of a FHIRPath" );
    return null;
  }
}

* Of course, we're talking about really generic FHIRPaths and not the very exact ones. Across the breadth of this topic, with my advice, your mileage may seriously vary.


FHIRPath examples

Extract the references from the first subject of an Observation:

Bundle.entry.resource.ofType(Observation).subject[0].reference.value

[Output] Patient/ebb16c62-cb06-4ff8-8ce8-ccb865e7a240

Extract the references from the first subject alternative:

Bundle.entry.resource.ofType(Observation).subject.first().reference.value

[Output] Patient/ebb16c62-cb06-4ff8-8ce8-ccb865e7a240

Select the first Patient reference where the diastolic is over 90:

Bundle.entry.resource.ofType(Observation).where(component.where(code.coding.code = '8462-4').value.where( value > 90.0).exists()).subject.reference.value.first()

[Output] Patient/5fabfccd-254e-42af-bed2-84199a5c05f2

Select the Patient's first name where the reference matches the logical id:

Bundle.entry.resource.ofType(Patient).where(id = '" + v.split("/")[1] + "').name.given.value

[Output] John

Complex FHIRPaths (with conditions to be met)

Imagine that you're parsing a Bundle with a Patient in it and you want to extract data from different places that cannot be defined by a simple FHIRPath. The highlighted lines are the ones where we'll either verify a match or extracting a datum:

<Patient xmlns="http://hl7.org/fhir">
   <id value="29632.PI"/>
   <identifier>
      <extension url="http://acme.us/fhir/extensions/mpid">
         <extension url="PIRecord">
            <valueString value="29632.PI"/>
         </extension>
      </extension>
      <system value="https://test.acme.io/mpi/0/"/>
      <value value="21240"/>
      <assigner>
         <display value="Acme Corporation Test"/>
      </assigner>
   </identifier>
   <identifier>
      <system value="https://github.com/synthetichealth/synthea"/>
      <value value="40b8f37e-385d-857f-2db0-07c142a8e4a2"/>
   </identifier>
   <name>
      <family value="Nikolaus26"/>
      <given value="Jesse626"/>
      <given value="Vincenzo126"/>
   </name>
   <birthDate value="1934-12-30"/>
</Patient>

Here are two entries in an imaginary YAML configuration file that lead us to extracting keys/fields named pi (patient index) and mpid (master patient identity):

pi:    Patient.identifier.where( $this.extension.url="http://acme.us/fhir/extensions/mpi" ).where( $this.extension.extension.url="PIRecord" ).extension.extension.value
mpid : Patient.identifier.where( $this.extension.url="http://acme.us/fhir/extensions/mpi" ).value

The first FHIRPath will extract 29632.PI and the second 21240.

Because extensions are in the realm of "user-defined" FHIR, it's obvious that the Acme Corporation is saying that it's keeping these two entities in a FHIR Identifier marked specifically with a nested extension that disambiguates the Identifier from other Identifiers that might exist. The FHIRPaths enforce this by their .where() clauses.

Let's read the first one (rule that produces pi)...

  1. Under a Patient is an identifier.
  2. FHIRPath function .where() imposes matching conditions:
  3. Under that Patient ($this) is an extension whose url contains the path http://acme.us/fhir/extensions/mpid.
  4. Also, under a second, nested extension, there is a url containing PIRecord.
  5. The conditions of #2 and #3 met, extract the value under the second extension deep.

For mpid, the extension-based condition of #2 is shared, but, back under the identifier is a value whose extracted datum provides mpid.


Troublshooting FHIRPath failures
Life is hard.
It's harder if you're stupid.

I don't mean to be demeaning or abrasive. These suggestions come at my own expense. When you're trying to analyze a FHIRPath against a FHIR resource and nothing you do works?

  1. Does your FHIRPath, match your resource? For instance, are trying to match a Patient path like
    Patient.indentifier.where( system="https://github.com/synthetichealth/synthea" )
    

    against a FHIR resource containing a Bundle?

    <Bundle xmlns="http://hl7.org/fhir">
      <entry>
        <resource>>
          <Patient xmlns="http://hl7.org/fhir">
            <id value="2877.PI" />
            <identifier>
              <system value="https://github.com/synthetichealth/synthea" />
              <value value="29e51479-f742-4474-8f8e-d2607d5269f6" />
            </identifier>
            <name>
              <use value="official" />
              <family value="Connelly992" />
              <given value="Abel832" />
              <prefix value="Mr." />
            </name>
          </Patient>
        </resource>
      </entry>
    </Bundle>
    
  2. Are you sensitive to the sometimes or apparent "arbitrary" nature of the terminating datum in a FHIRPath expression?
    Patient.birthDate.value                # as compared to
    Patient.name.text                      # (no value)
    Observation.category.coding.display    # no .value and...
    Observation.category.text              # no .value ...as compared to
    Observation.category.coding.code.value # which needs .value
    
    These take a lot of getting used to.

  3. Catch FHIRPathException in your code and what it says.
    List< Base > values;
    Object       value;
    
    try
    {
      values = fhirEngine.evaluate( resource, condition );
    
      if( !values.isEmpty() )
        value = values.get( 0 );
    }
    catch( FHIRException e )
    {
      logger.warn( e.getMessage() );
    }
    

Practical FHIRPath polymorphic nitty-gritty: groking what you read, what you see and what you do...

To avoid confusion, let's note these examples. These apply to all instances of polymorphism ([x]) in FHIR.

  1. The FHIR specification for a Patient carries field multipleBirth[x]: Patient. The field can be boolean or a date/time type.
    ...
    maritalStatus              0..1     CodeableConcept
    multipleBirth[x]           0..1
        multipleBirthBoolean            boolean
        multipleBirthInteger            integer
    photo                      0..*     Attachment
    ...
    
  2. But, in a FHIR document (XML here), this example of multipleBirth appears:
    <Patient  xmlns="http://hl7.org/fhir">
      ...
      <multipleBirthBoolean value="true" />  <!-- or, instead: -->
      <multipleBirthInteger value="3" />
      ...
    </Patient>
    
  3. However, the FHIRPath passed to the FHIRPath Engine in both cases is:
    Patient.multipleBirth
    
    ...and not anything obviously specific like Patient.multipleBirthBoolean nor any indication of [x]!

    FHIRPath writing rules:
    1. Don't write [x].
    2. Don't write the data type on the end (Boolean or Integer in the example above).
    3. Do write just the principal fieldname (multipleBirth).
    —because the FHIRPath will not evaluate to anything if you do not follow the three rules above!

    However, when composing FHIR in XML (also JSON), follow these rules:

    FHIR document writing rules:
    1. Do write the data type on the end of the principal fieldname (just as you see above).

    In the (IntelliJ IDEA) debugger, you will see:

    1. Just the principal fieldname with, in parentheses,
    2. the data type and
    3. subsumed below all the different field data you'd expect from the data type.

  4. I still have to think a little about this when I write a test FHIR document (#2), a FHIRPath to nail a datum in it (#3) and contrast #2 and #3 above with the FHIR specification (#1), to wit, the multiple birth field.

  5. The (HAPI FHIR) Java code to gather the evaluation is (line 11) below. (Note that the evalution of a FHIRPath can easily result
    in 0, 1 or even multiple values. Usually, however, there is only one value, line 17, the first one, that interests what we're doing.)
    import org.hl7.fhir.r4.model.Base;
    import org.hl7.fhir.r4.model.BooleanType;
    import org.hl7.fhir.r4.model.IntegerType;
    import org.hl7.fhir.r4.model.Resource;
    import org.hl7.fhir.r4.utils.FHIRPathEngine;
    
    private FHIRPathEngine fhirEngine;
    
    void extract( Resource resource, String fhirPath )
    {
      List< Base > values = fhirEngine.evaluate( resource, fhirPath );
    
      if( values.isEmpty() )
        fail( "No datum was reached using FHIRPath \" + fhirPath + "\"" );
    
      String value  = null;
      Object object = values.get( 0 );
    
      // which of boolean or integer was it?
      if( object instanceof BooleanType )
        value = ( ( BooleanType ) object ).asStringValue();
      else if( object instance of IntegerType )
        value = ( ( IntegerType ) object ).getValueAsString();
    
      ...
    
  6. Practical application code extending the above means using value plus perhaps fhirPath to express what you found.

Primitive FHIR datatypes...

For reference, here are the primitive FHIR datatypes. An instance of any of these should return true in the following execution:

Base object = ( one of these );

if( object.isPrimitive() ) ...

To see the value held by such an object, the following will convert it to a string:

String value = ( ( PrimitiveType< ? > ) value).asStringValue();

Practical FHIRPath, part 2

There are two problems with FHIRPath. Let's illustrate by pretending we're implementing the creation of "fieldnames" plus FHIRPaths into a resource to give those fieldnames a value. For our example, let's assume FHIR Observation and, within that, we want effectiveDateTime plus interpretation.

  1. The end element resolves to a simple datum type, let's call it a primitive base type like BooleanType, IntegerType, StringType, DecimalType, etc. This is nice, but we have to "dig into it" nevertheless to convert it to a Java String. That's what we're doing in the code below (line 48), but here is our FHIRPath:
    effectiveDate :   Observation.effective
    
  2. The end element resolves to some "nightmare," that is, not at all simple, scalar type like CodeableConcept. Without writing Java code to detect that and create some impossible string representation of everything in there, you have to go back and tell the author not to do such a stupid thing, but grow a brain and also an ability in FHIRPath to handle it himself (line 35). Here is our FHIRPath:
    interpretation :  Observation.interpretation
    
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r4.model.Base;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.utils.FHIRPathEngine;

public class Foo
{
  public void fooBar( Resource resource, final String fhirPath )
  {
    List< Base > values;

    try
    {
      values = fhirEngine.evaluate( resource, fhirPath );
    }
    catch( FHIRException e )
    {
      StringBuilder message = new StringBuilder();
      message.append( e.getMessage() ).append( ":\n    " ).append( fhirPath ).append( "\nSkipping..." );
      logger.debug( message.toString() );
      return;
    }

    if( values.isEmpty() )            // (FHIRPath resolved to no datum)
      continue;

    Base   object = values.get( 0 );
    String value  = object.toString();

    if( !object.isPrimitive() )      // this is pretty clever...
    {
      StringBuilder message = new StringBuilder();
      message.append( "Fieldname: " ).append( fieldname )
           .append( ", FHIRPath: " ).append( fhirPath );
      message.append( System.lineSeparator() );
      message.append( "    Object is not a primitive type and cannot be the terminal element of a FHIRPath. " );
      message.append( "It will be necessary to resort to a more complex FHIRPath. Skipping..." );
      continue;
    }

    try                              // ...and this is even more clever!
    {
      value = ( ( PrimitiveType< ? > ) object ).asStringValue();
    }
    catch( Exception e )
    {
      logger.warn( "Failed to convert {} to simple string for {}, because ", object, fhirPath, e );
      continue;
    }
  }
}

Note that Observation's effective[x] includes

  1. effectiveDateTime
  2. effectivePeriod
  3. effectiveTiming
  4. effectiveInstant

Of the 4 different possibilities for what's to be put there, two of them are pretty simple to translate (1 and 4) using this FHIRPath:

Observation.effective

The other two require much more work, better done in FHIRPath than in Java. Here is an example for getting the starting date of the observation if what's recorded is a Period:

Observation.effective.ofType( Period ).start

This works because start is one of those easily handled scalars (DateTimeType), so the FHIRPath can stop there.


FHIRPath functions

"Functions" are syntactically expressed facilities that appear in the FHIRPath and aid in reaching a particular datum wanted.

Here is a summary to tease the imagination, but you can find the definitive treatment here: FHIRPath.

  1. extension(url : string) : collection

    Filter the input collection for items named "extension" with the given url. This is a syntax shortcut for .extension.where(url = string), but is simpler to write. Will return an empty collection if the input collection is empty or the url is empty.

  2. hasValue() : Boolean

    Returns true if the input collection contains a single value which is a FHIR primitive.

  3. getValue() : System.[type]

    Return the underlying system value for the FHIR primitive if the input collection contains a single value which is a FHIR primitive, and it has a primitive value (see discussion for hasValue()). Otherwise the return value is empty.

  4. resolve() : collection

    For each item in the collection, if it is a string that is a uri (or canonical or url), locate the target of the reference, and add it to the resulting collection.

  5. ofType(type : identifier) : collection

    An alias for ofType() maintained purely for backwards compatibility.

  6. ofType(type : identifier) : collection

    Returns a collection that contains all items in the input collection that are of the given type or a subclass thereof.

  7. elementDefinition() : collection

    Returns the FHIR element definition information for each element in the input collection.

  8. slice(structure : string, name : string) : collection

    Returns the given slice as defined in the given structure definition. The structure argument is a uri.

  9. checkModifiers(modifier : string) : collection

    For each element in the input collection, verifies that there are no modifying extensions defined other than the ones given by the modifier argument (comma-separated string).

  10. conformsTo(structure : string) : Boolean

    Returns true if the single input element conforms to the profile specified by the structure argument, and false otherwise.

  11. memberOf(valueset : string) : Boolean

    When invoked on a single code-valued element, returns true if the code is a member of the given valueset.

  12. subsumes(code : Coding | CodeableConcept) : Boolean

    When invoked on a Coding-valued element and the given code is Coding-valued, returns true if the source code is equivalent to the given code, or if the source code subsumes the given code.

  13. subsumedBy(code: Coding | CodeableConcept) : Boolean

    When invoked on a Coding-valued element and the given code is Coding-valued, returns true if the source code is equivalent to the given code.

  14. htmlChecks : Boolean

    When invoked on a single xhtml element returns true if the rules around HTML usage are met, and false if they are not.

  15. lowBoundary : T

    This function returns the lowest possible value in the natural range expressed by the type it is invoked on.

  16. highBoundary : T

    This function returns the lowest possible value in the natural range expressed by the type it is invoked on.

  17. comparable(quantity) : boolean

    This function returns true if the engine executing the FHIRPath statement can compare the singleton Quantity with the singleton other Quantity and determine their relationship to each other.