hapifhir / hapi-fhir

🔥 HAPI FHIR - Java API for HL7 FHIR Clients and Servers
http://hapifhir.io
Apache License 2.0
1.99k stars 1.31k forks source link

Extensions on primitive elements get lost when (de-)serialized #2801

Open UMEihle opened 3 years ago

UMEihle commented 3 years ago

Description Extensions on primitive elements (https://www.hl7.org/fhir/json.html#primitive) get lost when serialized and after that, deserialized again. Especially with Camunda process variables this behavior is not good as Camunda can (de-)serialize process variables. https://docs.camunda.org/manual/7.15/user-guide/process-engine/variables/#java-object-api

To Reproduce

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.parser.IParser;
import org.hl7.fhir.r4.model.*;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collections;

public class Test {

    public static void main(String[] args) throws Exception {

        // Build patient
        Patient patient = getPatient();

        // Write patient to file
        FileOutputStream fout = new FileOutputStream("patient.ser");
        ObjectOutputStream oos = new ObjectOutputStream(fout);
        oos.writeObject(patient);

        FhirContext ctx = FhirContext.forR4();
        IParser jsonParser = ctx.newJsonParser();
        jsonParser.setPrettyPrint(true);
        String jsonPatient1 = jsonParser.encodeResourceToString(patient);
        System.out.println("-------------------Resource written to file: ");
        System.out.println(jsonPatient1);

        // Read patient from file
        FileInputStream streamIn = new FileInputStream("patient.ser");
        ObjectInputStream objectinputstream = new ObjectInputStream(streamIn);
        Patient p2 = (Patient) objectinputstream.readObject();

        String jsonPatient2 = jsonParser.encodeResourceToString(p2);
        System.out.println("\n-------------------Resource read from file: ");
        System.out.println(jsonPatient2);

    }

    private static DateType getPatientBirthDate() throws ParseException {
        DateTimeType birthDateTime = new DateTimeType(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("1974-12-25 14:35:45"));
        Extension extension = new Extension("http://hl7.org/fhir/StructureDefinition/patient-birthTime").setValue(birthDateTime);
        DateType result = new DateType(new SimpleDateFormat("yyyy-MM-dd").parse("1974-12-25"));
        result.setExtension(Collections.singletonList(extension));
        return result;
    }

    private static Patient getPatient() throws ParseException {
        Patient patient = new Patient();
        patient.setId("example-patient");
        DateType birthDate = getPatientBirthDate();
        return patient.setBirthDateElement(birthDate);
    }

}

Output

-------------------Resource written to file: 
{
  "resourceType": "Patient",
  "id": "example-patient",
  "birthDate": "1974-12-25",
  "_birthDate": {
    "extension": [ {
      "url": "http://hl7.org/fhir/StructureDefinition/patient-birthTime",
      "valueDateTime": "1974-12-25T14:35:45+01:00"
    } ]
  }
}

-------------------Resource read from file: 
{
  "resourceType": "Patient",
  "id": "example-patient",
  "birthDate": "1974-12-25"
}

Expected behavior FHIR resource objects should be the same after (de-)serialization.

Environment

vadi2 commented 5 months ago

Another test case using HAPI 7.0.2:

package com.test.test

import ca.uhn.fhir.context.FhirContext
import ca.uhn.fhir.parser.IParser
import org.hl7.fhir.r4.model.Patient
import org.junit.jupiter.api.Test
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.ObjectInputStream
import java.io.ObjectOutputStream

class Test {

    val fhirString = """

        {
            "resourceType": "Patient",
            "address": [
                {
                    "type": "both",
                    "_line": [
                        {
                            "extension": [
                                {
                                    "url": "http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-houseNumber",
                                    "valueString": "4"
                                },
                                {
                                    "url": "http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-streetName",
                                    "valueString": "Doktor Sven Johanssons Backe"
                                }
                            ]
                        },
                        {
                            "extension": [
                                {
                                    "url": "http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-additionalLocator",
                                    "valueString": "It's very hilly"
                                }
                            ]
                        }
                    ],
                    "line": [
                        "Doktor Sven Johanssons Backe 4",
                        "It's very hilly"
                    ],
                    "city": "Göteborg",
                    "country": "SE"
                }
            ]
        }
    """.trimIndent()

    @Test
    fun test() {
        val fhirContext: FhirContext = FhirContext.forR4()
        val fhirParser: IParser = fhirContext.newJsonParser()
        val org = fhirParser.parseResource(fhirString) as Patient

        assert(org.address.first().line.first().extension.size == 2) { "Didn't have in org" }

        println("original serialized ${fhirParser.encodeResourceToString(org)}")

        val fileOutputStream = ByteArrayOutputStream()
        val objectOutputStream = ObjectOutputStream(fileOutputStream)
        objectOutputStream.writeObject(org)
        objectOutputStream.flush()
        val array = fileOutputStream.toByteArray()
        objectOutputStream.close()

        val fileInputStream = ByteArrayInputStream(array)
        val objectInputStream = ObjectInputStream(fileInputStream)
        val copy = objectInputStream.readObject() as Patient
        objectInputStream.close()

        println(fhirParser.encodeResourceToString(copy))
        assert(copy.address.first().line.first().extension.size == 2) { "Didn't have in copy" }

    }
}
plugins {
    kotlin("jvm") version "1.9.22"
}

group = "org.example"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

dependencies {
    implementation("ca.uhn.hapi.fhir:hapi-fhir-base:7.0.2")
    implementation("ca.uhn.hapi.fhir:hapi-fhir-structures-r4:7.0.2")

    testImplementation(platform("org.junit:junit-bom:5.10.2"))
    testImplementation("org.junit.jupiter:junit-jupiter")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")

    testImplementation("org.jetbrains.kotlin:kotlin-test")
}

tasks.test {
    useJUnitPlatform()
}
kotlin {
    jvmToolchain(20)
}
vadi2 commented 5 months ago

Not an issue in HAPI per-se, instead the correct serialization method needs to be used: https://hapifhir.io/hapi-fhir/docs/model/parsers.html#encoding-aka-serializing