FHIR Validation

Russell Bateman
April 2021
last update:



I thought I'd leave some bread crumbs on this. Some of the type choices look a little weird; that's be cause this code came from a large work using streams and bytes instead of strings.

Ascertaining FHIR format...

public void method()
{
  InputStreamReader isReader = new InputStreamReader( inputStream );
  BufferedReader    reader   = new BufferedReader( isReader );
  String            format   = reader.lines().collect( Collectors.joining( "\n" ) ) );
}

Parsing FHIR...

...using format obtained earlier.

public void method()
{
  FhirContext      fhirContext    = FhirContext.forR4();
  IParser          fhirXmlParser  = fhirContext.newXmlParser().setPrettyPrint( true );
  IParser          fhirJsonParser = fhirContext.newJsonParser().setPrettyPrint( true );
  IParser          parser         = ( format.equals( "XML" ) ) ? fhirXmlParser : fhirJsonParser;
  IBaseResource    resource       = parser.parseResource( inputStream );
  byte[]           document       = parser.encodeResourceToString( resource).getBytes() )
}

FHIR instance validation...

...using fhirContext obtained earlier. Note: you will want to consider the information about line- and column numbers in the Appendix below. Here we do it using a Java object for the resource, but there's a better way. In the meantime, ...

public void method()
{
  ValidationSupportChain supportChain      = new ValidationSupportChain(
                                      new DefaultProfileValidationSupport( fhirContext ),
                                      new InMemoryTerminologyServerValidationSupport( fhirContext ),
                                      new CommonCodeSystemsTerminologyService( fhirContext ) );
  FhirValidator          validator         = fhirContext.newValidator().registerValidatorModule( fhirModule );
  FhirInstanceValidator  instanceValidator = new FhirInstanceValidator( supportChain );
                                      instanceValidator.setAnyExtensionsAllowed( true );
  ValidationResult       results           = validator.validateWithResult( resource );
}

Printing out validation errors...

...using results and document obtained earlier. (There's simpler code to be found in the Appendix below.)

import java.io.ByteArrayInputStream;
import java.io.InputStreamReader;

import ca.uhn.fhir.validation.SingleValidationMessage;
import ca.uhn.fhir.validation.ValidationResult;

import com.windofkeltia.utilities.StringUtilities;

  protected void outputPlainText() throws IOException
  {
    StringBuilder sb = new StringBuilder();

    System.out.print( "Validation Results\n\n" );

    sb.append( "  Validation " );
    if( results.isSuccessful() )
      sb.append( "succeeded.\n\n" );
    else
      sb.append( "failed with " )
        .append( results.getMessages().size() )
        .append( " errors, warnings or other notes.\n\n" );
    System.out.print( sb.toString() );
    sb.setLength( 0 );

    if( !results.isSuccessful() )
    {
      sb.append( "  " )
        .append( StringUtilities.padStringRight( "    ", 4 ) )
        .append( StringUtilities.padStringRight( "Severity", 11 ) )
        .append( "Location (FHIRPath)    " );
        .append( "Profile, Element and Problem description\n" );
        .append( "  " )
        .append( StringUtilities.padStringRight( "    ", 4 ) )
        .append( StringUtilities.padStringRight( "--------", 11 ) )
        .append( "-------------------    " );
        .append( "----------------------------------------\n" );
      System.out.print( sb.toString() );
      sb.setLength( 0 );

      int count = 1;

      for( SingleValidationMessage message : results.getMessages() )
      {
        sb.setLength( 0 );
        sb.append( "  " )
          .append( StringUtilities.padStringRight( ""+count++, 4 ) )
          .append( StringUtilities.padStringRight( message.getSeverity().toString(), 11 ) )
          .append( message.getLocationString() ).append( "   " )
          .append( message.getMessage() ).append( '\n' );
        System.out.print( sb );
      }
    }

    System.out.print( "\n\nContent of Validated Document\n\n" );
    outputDocumentWithLineNumbers( document );
  }

  private void outputDocumentWithLineNumbers( final byte[] document ) throws IOException
  {
    BufferedReader reader = new BufferedReader( new InputStreamReader( new ByteArrayInputStream( document ) ) );
    String         line;
    int            lineNumber = 1;

    while( nonNull( line = reader.readLine() ) )
    {
      System.out.print( StringUtilities.padStringLeft( lineNumber+": ", 5 ) );
      System.out.print( line );
      System.out.print( '\n' );
      lineNumber++;
    }
  }

Appendix: line- and column numbers

Based on a suggestion James Agnew made, I figured out that the absence of useful FHIR validator line and column numbers is explainable and remediable. If you validate an actual Java resource like

Bundle bundle = new Bundle()...

...and fill it with contents, then pass it to the HAPI FHIR validator, HAPI FHIR will first serialize the resource (which will of course result in a string with no newlines, indentation, etc.). So, when you get the SingleValidationMessage from the results, then ask for the line number, you always get 1 (duh).

When you ask for the column number, if you apply what comes back to your (nice, pretty, mental) representation of the FHIR resource—all neatly indented, etc.—the column number corresponds to nothing logical even if you realize that it's relative to the beginning of the (only) line.

The solution is to eschew the validateWithResult( IBaseResource ) method and, instead, use your neatly indented, pretty-printed FHIR resource, the one a human being would find pleasant to look at, passed to the validateWithResult( String ) method.

Armed with this understanding, you can put line numbers and columns back into the validation results with spectacular effect by doing

/**
 * @param some pretty XML or JSON representation
 */
public ValidationResult getResultsFromString( final String CONTENT )
{
  FhirContext            context           = new FhirContext( FhirVersionEnum.R5 );
  ValidationSupportChain supportChain      = new ValidationSupportChain(
                                        new DefaultProfileValidationSupport( context ),
                                        new InMemoryTerminologyServerValidationSupport( context ),
                                        new CommonCodeSystemsTerminologyService( context ) );
  IValidatorModule       module            = new FhirInstanceValidator( context );
  FhirValidator          validator         = context.newValidator().registerValidatorModule( module );
  FhirInstanceValidator  instanceValidator = new FhirInstanceValidator( supportChain );
  instanceValidator.setAnyExtensionsAllowed( true );

  return validator.validateWithResult( CONTENT );
}

...instead of

/**
 * @param some Java object representation
 */
public ValidationResult getResultsFromString( final IBaseResource RESOURCE )
{
  FhirContext            context           = new FhirContext( FhirVersionEnum.R5 );
  ValidationSupportChain supportChain      = new ValidationSupportChain(
                                        new DefaultProfileValidationSupport( context ),
                                        new InMemoryTerminologyServerValidationSupport( context ),
                                        new CommonCodeSystemsTerminologyService( context ) );
  IValidatorModule       module            = new FhirInstanceValidator( context );
  FhirValidator          validator         = context.newValidator().registerValidatorModule( module );
  FhirInstanceValidator  instanceValidator = new FhirInstanceValidator( supportChain );
  instanceValidator.setAnyExtensionsAllowed( true );

  return validator.validateWithResult( RESOURCE );
}

The imports for all of the above:

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
import ca.uhn.fhir.validation.FhirValidator;
import ca.uhn.fhir.validation.IValidatorModule;
import ca.uhn.fhir.validation.SingleValidationMessage;
import ca.uhn.fhir.validation.ValidationResult;
import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService;
import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport;
import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain;
import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator;

For the utility of it, let's print these validation results to the console:

public static void formatOutput( ValidationResult results )
{
  StringBuilder sb    = new StringBuilder();
  int           count = 1;

  for( SingleValidationMessage message : results.getMessages() )
  {
    sb.append( "  " )
      .append( count )
      .append( message.getSeverity().toString() )
      .append( message.getLocationString() ).append( "   " )
      .append( message.getLocationLine() ).append( ',' )
      .append( message.getLocationCol() ).append( "   " )
      .append( message.getMessage() ).append( '\n' );
    System.out.print( sb );
    sb.setLength( 0 );
  }
}

...and, finally, the sample output, something like:

      Severity Location                                           Line,Column Message
  1   ERROR    Bundle                                                1,37     bdl-3: 'entry.request mandatory for batch/transaction/history, allowed for subscription-notification, otherwise prohibited' failed
  2   ERROR    Bundle.entry[0].resource.ofType(Patient).id          10,33     As specified by profile http://hl7.org/fhir/StructureDefinition/Patient, Element 'id' is out of order
  3   ERROR    Bundle.entry[0].resource.ofType(Patient).telecom[0]  25,18     As specified by profile http://hl7.org/fhir/StructureDefinition/Patient, Element 'telecom' is out of order
  4   ERROR    Bundle.entry[1].resource.ofType(Encounter)           34,15     Encounter.status: minimum required = 1, but only found 0 (from http://hl7.org/fhir/StructureDefinition/Encounter)
  5   ERROR    Bundle.entry[1].resource.ofType(Encounter)           34,15     Encounter.class: minimum required = 1, but only found 0 (from http://hl7.org/fhir/StructureDefinition/Encounter)
  6   ERROR    Bundle.entry[2].resource.ofType(Encounter)           45,15     Encounter.status: minimum required = 1, but only found 0 (from http://hl7.org/fhir/StructureDefinition/Encounter)
  7   ERROR    Bundle.entry[2].resource.ofType(Encounter)           45,15     Encounter.class: minimum required = 1, but only found 0 (from http://hl7.org/fhir/StructureDefinition/Encounter)

This was the content to be validated:

<Bundle xmlns="http://hl7.org/fhir">
  <type value="transaction"/>
  <entry>
    <fullUrl value="urn:uuid:38826a3f-8e5c-4846-95c3-7f12a233447d" />
    <resource>
      <Patient xmlns="http://hl7.org/fhir">
        <meta>
          <lastUpdated value="2021-01-05T00:00:00.000-06:00" />
        </meta>
        <id value="9812850.PI"/>
        <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>
    <resource>
      <Encounter xmlns="http://hl7.org/fhir">
        <id value="emerg" />
        <period>
          <start value="2017-02-01T08:45:00+10:00" />
          <end value="2017-02-01T09:27:00+10:00" />
        </period>
      </Encounter>
    </resource>
  </entry>
  <entry>
    <resource>
      <Encounter xmlns="http://hl7.org/fhir">
        <id value="emerg" />
        <period>
          <start value="2019-11-01T08:45:00+10:00" />
          <end value="2019-11-01T09:27:00+10:00" />
        </period>
      </Encounter>
    </resource>
  </entry>
</Bundle>