FHIR XML-to-JSON or JSON-to-XML Converter
|
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.
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:
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():
...and, just as conveniently, two versions of IParser.encodeResourceXyz():
/* ================================================================= * 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; } }
/* ================================================================= * 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 ); } }
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>
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
...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