FHIR XML-to-JSON or JSON-to-XML Converter

Russell Bateman
October 2020
last update:



This is a stand-alone application that relies upon HAPI FHIR. It demonstrates how easily FHIR documents may be changed from JSON to XML or from XML to JSON using the HAPI FHIR library.

About what we're doing in HAPI FHIR here...

If you came here to find out only what's involved in handling JSON and XML in FHIR, check out the highlighted lines below. Five lines of code are all that constitute parsing the incoming document in either JSON or XML, then outputing it in the opposing format (XML or JSON). Here are the simple steps:

  1. Create HAPI FHIR context. This is somewhat costly; do it once only—just reuse this context.
  2. Knowing the input format (JSON or XML), create a HAPI FHIR parser. If you really want to optimize, you can hang on to this parser too.
  3. Run the parser to get a handle to the FHIR resource parsed. This is the bit that must be done for every new document.
  4. Create a HAPI FHIR parser of the target format (XML or JSON). Again, you could hang on to this parser and reuse it.
  5. Using the second parser, encode what was parsed, maybe pretty print it, then turn it out as the answer.

When you run the HAPI FHIR parser, if you don't know the type of resource that's in the document and you don't care (as we do not here since we're converting whatever happens to be there), just use IBaseResource. If we did care, we might substitute Bundle or Patient or MedicationRequest, etc. in place of IBaseResource.

Note that conveniently there are three versions of IParser.parseResource():

  1. IBaseResource parseResource( String string ) —used here
  2. IBaseResource parseResource( InputStream inputStream )
  3. IBaseResource parseResource( Reader reader )

...and, just as conveniently, two versions of IParser.encodeResourceXyz():

  1. String encodeResourceToString( IBaseResource resource )
  2. void encodeResourceToWriter( IBaseResource resource, Writer writer )

...or, if you want, just skip to the code:

src/main/java/com/windofkeltia/fhir/FhirConvert.java:
/* =================================================================
 * Copyright (C) 2020 by Russell Bateman and Etretat Logiciels, LLC.
 * All rights and free use are granted.
 * =================================================================
 */
package com.windofkeltia.fhir;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

import static java.util.Objects.isNull;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.parser.IParser;
import org.hl7.fhir.instance.model.api.IBaseResource;

/**
 * Translate FHIR XML to JSON or JSON to XML.
 * @author Russell Bateman
 * @since October 2020
 */
public class FhirConvert
{
  private static boolean toXml = false;

  private static final FhirContext context = FhirContext.forR4();

  /**
   * @param args a list of files and/or subdirectories of files to convert.
   */
  public static void main( String[] args ) throws IOException
  {
    if( args[ 0 ].equals( "--help" ) )
    {
      System.out.println( "FhirConvert:   Translates FHIR JSON to FHIR XML or FHIR XML to FHIR JSON" );
      System.out.println( "Command line:  FhirConvert xml|json files|subdirectories" );
      System.out.println( "Output result: whatever the incoming filename was, the same name is created"
                        + " switching (or adding to) its extension for the new format (.xml or .json)" );
      return;
    }

    for( String argument : args )
    {
      switch( argument.toLowerCase() )
      {
        case "xml" :  toXml = true;  continue;
        case "json" : toXml = false; continue;
      }

      File    entity      = new File( argument );
      boolean exists      = entity.exists();
      boolean isDirectory = entity.isDirectory();

      if( !exists )
      {
        System.err.println( "Argument \"" + argument + "\" does not exist; skipping..." );
        continue;
      }

      if( isDirectory )
      {
        File[] subdirectory = new File( argument ).listFiles();

        if( isNull( subdirectory ) )
          break;

        for( File file : subdirectory )
        {
          if( file.isDirectory() )
            continue;

          convertFhirDocuments( argument );
        }
      }
      else
      {
        convertFhirDocuments( argument );
      }
    }
  }

  /** Do the heavy lifting here. */
  private static void convertFhirDocuments( final String filepath ) throws IOException
  {
    String content = getLinesInFile( filepath );

    try
    {
      String converted = ( toXml ) ? convertJsonToXmlFhir( content ) : convertXmlToJsonFhir( content );
      System.out.println( filepath + " ..." );
      writeOutConvertedContent( filepath, converted );
    }
    catch( DataFormatException e )
    {
      System.err.println( "Skipping " + filepath + ":" + e.getMessage() );
    }
    catch( IOException e )
    {
      System.err.println( "Failed to write out converted " + filepath + ":" + e.getMessage() );
    }
  }

  /** Assume incoming content is JSON; generate outgoing content as XML. */
  private static String convertJsonToXmlFhir( final String content ) throws DataFormatException
  {
    IParser       source   = context.newJsonParser();                        // new JSON parser
    IBaseResource resource = source.parseResource( content );                // parse the resource
    IParser       target   = context.newXmlParser();                         // new XML parser
    return target.setPrettyPrint( true ).encodeResourceToString( resource ); // output XML
  }

  /** Assume incoming content is XML; generate outgoing content as JSON. */
  private static String convertXmlToJsonFhir( final String content ) throws DataFormatException
  {
    IParser       source   = context.newXmlParser();                         // new XML parser
    IBaseResource resource = source.parseResource( content );                // parse the resource
    IParser       target   = context.newJsonParser();                        // new JSON parser
    return target.setPrettyPrint( true ).encodeResourceToString( resource ); // output JSON
  }

  /**
   * Read the specified file into a string.
   * @param pathname full path to the file.
   * @return the file's contents as a string.
   * @throws IOException thrown when file I/O goes bad.
   */
  private static String getLinesInFile( final String pathname ) throws IOException
  {
    StringBuilder sb = new StringBuilder();

    try ( BufferedReader br = new BufferedReader( new FileReader( pathname ) ) )
    {
      for( String line = br.readLine(); line != null; line = br.readLine() )
        sb.append( line ).append( '\n' );
    }

    return sb.toString();
  }

  /** From the original filename, concoct the one receiving the converted content. */
  protected static String createOutputName( final String path )
  {
    final String incoming = ( toXml ) ? "json" : "xml";
    final String outgoing = ( toXml ) ? "xml"  : "json";
    String outputName = path.replace( incoming, outgoing );
    if( !outputName.endsWith( ( toXml ) ? "xml" : "json" ) )
      outputName += ( toXml ) ? ".xml" : ".json";
    return outputName;
  }

  /**
   * Write the converted content out under a new name.
   * @param path full pathname to the file that was converted; don't overwrite it!
   * @param content converted content (XML or JSON) to write out to a new file.
   * @throws IOException thrown when file I/O goes bad.
   */
  private static void writeOutConvertedContent( final String path, final String content ) throws IOException
  {
    final String outputName = createOutputName( path );
    try( BufferedWriter writer = new BufferedWriter( new FileWriter( outputName ) ) )
    {
      writer.write( content );
    }
  }

  /** For unit-testing purposes only. */
  protected static void setToXml( boolean trueFalse ) { toXml = trueFalse; }
}

Test first, write code second!

src/test/java/com/windofkeltia/fhir/FhirConvertTest.java:
/* =================================================================
 * Copyright (C) 2020 by Russell Bateman and Etretat Logiciels, LLC.
 * All rights and free use are granted.
 * =================================================================
 */
package com.windofkeltia.fhir;

import java.io.IOException;

import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;

import static org.junit.Assert.assertEquals;

/**
 * @author Russell Bateman
 * @since October 2020
 */
@FixMethodOrder( MethodSorters.JVM )
public class FhirConvertTest
{
  private static final boolean VERBOSE = false;

  @Test
  public void testHelp() throws IOException
  {
    String[] args = { "--help" };
    FhirConvert.main( args );
    System.out.println();
  }

  @Test
  public void testJsonToXml() throws IOException
  {
    String[] args = { "XML", "src/test/resources/fodder/fhir-1.json" };
    FhirConvert.main( args );
  }

  @Test
  public void testXmlToJson() throws IOException
  {
    String[] args = { "JSON", "src/test/resources/fodder/fhir-1.xml" };
    FhirConvert.main( args );
  }

  @Test
  public void testJsonToXml2() throws IOException
  {
    String[] args = { "XML", "src/test/resources/fodder/fhir-2.json" };
    FhirConvert.main( args );
  }

  @Test
  public void testXmlToJson2() throws IOException
  {
    String[] args = { "JSON", "src/test/resources/fodder/fhir-2.xml" };
    FhirConvert.main( args );
  }

  @Test
  public void testCreateXmlOutputName()
  {
    FhirConvert.setToXml( true );
    String inputName  = "filename.json";
    String outputName = FhirConvert.createOutputName( inputName );
    if( VERBOSE )
      System.out.println( inputName + " -> " + outputName );
    assertEquals( "filename.xml", outputName );
  }

  @Test
  public void testCreateXmlOutputName2()
  {
    FhirConvert.setToXml( true );
    String inputName  = "filename";
    String outputName = FhirConvert.createOutputName( inputName );
    if( VERBOSE )
      System.out.println( inputName + " -> " + outputName );
    assertEquals( "filename.xml", outputName );
  }

  @Test
  public void testCreateJsonOutputName()
  {
    FhirConvert.setToXml( false );
    String inputName  = "filename";
    String outputName = FhirConvert.createOutputName( inputName );
    if( VERBOSE )
      System.out.println( inputName + " -> " + outputName );
    assertEquals( "filename.json", outputName );
  }
}

Build a stand-alone, executable JAR...

At the time I created this tutorial, the versions used in <properties> were pretty much the latest.

<project xmlns="http://maven.apache.org/POM/4.0.0"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                      http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.windofkeltia.fhir</groupId>
  <artifactId>fhir-convert</artifactId>
  <version>1.0.0-SNAPSHOT</version>
  <description>FHIR JSON-to-XML-to-JSON Converter</description>
  <packaging>jar</packaging>

  <properties>
    <hapi-fhir.version>5.1.0</hapi-fhir.version>
    <junit.version>4.12</junit.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven-jar-plugin.version>3.2.0</maven-jar-plugin.version>
    <maven-assembly-plugin.version>3.3.0</maven-assembly-plugin.version>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>ca.uhn.hapi.fhir</groupId>
      <artifactId>hapi-fhir-base</artifactId>
      <version>${hapi-fhir.version}</version>
    </dependency>
    <dependency>
      <groupId>ca.uhn.hapi.fhir</groupId>
      <artifactId>hapi-fhir-structures-r4</artifactId>
      <version>${hapi-fhir.version}</version>
    </dependency>
    <!-- shut slf4j up from complaining...
        ...but log4j will complain without configuration! -->
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-log4j12</artifactId>
      <version>1.7.30</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>${junit.version}</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-assembly-plugin</artifactId>
        <version>${maven-assembly-plugin.version}</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>single</goal>
            </goals>
            <configuration>
              <archive>
                <manifest>
                  <mainClass>com.windofkeltia.fhir.FhirConvert</mainClass>
                </manifest>
              </archive>
              <descriptorRefs>
                <descriptorRef>jar-with-dependencies</descriptorRef>
              </descriptorRefs>
              <appendAssemblyId>false</appendAssemblyId>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>
src/main/resources/log4j.properties:

This will shut log4j up from complaining plus turn on some nice warnings as HAPI FHIR does its job. If you want to turn it off completely, replace WARN with OFF. Otherwise, you can choose between TRACE, DEBUG, WARN, INFO, ERROR and FATAL as logging levels. Logging comes out of HAPI FHIR (because FhirConvert.java does no logging).

# Set root logger level to DEBUG and its only appender to Console.
log4j.rootLogger=WARN, Console

# Console is set to be a ConsoleAppender.
log4j.appender.Console=org.apache.log4j.ConsoleAppender

# Console uses PatternLayout.
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
log4j.appender.Console.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n

The JAR that comes out is ./target/fhir-convert-1.0.0-SNAPSHOT.jar. Run it like this:

$ java -jar fhir-convert-1.0.0-SNAPSHOT.jar --help
FhirConvert:   Translates FHIR JSON to FHIR XML or FHIR XML to FHIR JSON
Command line:  FhirConvert xml|json files|subdirectories
Output result: whatever the incoming filename was, the same name is created switching (or adding to) its extension for the new format (.xml or .json)
$ java -jar fhir-convert-1.0.0-SNAPSHOT.jar fhir-1.json

Some JSON examples...

...would look something like this:

{
  "resourceType" : "Bundle",
  "type" :         "transaction",
  "entry" :
  [
    {
      "fullUrl" :  "urn:uuid:d67a0cff-8f7e-46f7-bb99-577a6623e59a",
      "resource" :
      {
        "resourceType" : "Patient",
        "id" :           "d67a0cff-8f7e-46f7-bb99-577a6623e59a",
        "text" :
        {
          "status" : "generated",
          "div" :    "<div xmlns=\"http://www.w3.org/1999/xhtml\">Generated by <a href=\"https://github.com/synthetichealth/synthea\">Synthea</a></div>"
        }
      }
    }
  ]
}

...and might come out like this when translated to XML:

<Bundle xmlns="http://hl7.org/fhir">
  <type value="transaction"></type>
  <entry>
    <fullUrl value="urn:uuid:d67a0cff-8f7e-46f7-bb99-577a6623e59a"></fullUrl>
    <resource>
      <Patient xmlns="http://hl7.org/fhir">
        <text>
          <status value="generated"></status>
          <div xmlns="http://www.w3.org/1999/xhtml">Generated by
            <a href="https://github.com/synthetichealth/synthea">Synthea</a>
          </div>
        </text>
      </Patient>
    </resource>
  </entry>
</Bundle>

Here is a good place to find some FHIR content. Click on the green Code↓ button and download the zip. You'll find the JSON example inside for several FHIR versions.


SYNTHEA: smart-on-fhir/generated-sample-data