FHIRPath notes

Russell Bateman
February 2020
last update:





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 FHIRPathe rather than simply XPath is because FHIR can be expressed several formats, like JSON, and not just XML.

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.


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.