Parsing FHIR Objects with Custom JSON Converters in C#
Published Mar 23 2022 02:12 PM 9,637 Views
Microsoft

pjirsa_0-1648068763242.png

Introduction

Working with complex JSON data can be quite cumbersome in a strongly typed language like C#. Especially if you want to deserialize that data into POCO model classes. Firely.NET offers a library of model classes for FHIR. However, when writing an ASP.NET WebAPI or Azure Function the default System.Text.Json serializer can get tripped up by the incoming and outgoing objects.

 

For example: Here is a sample FHIR Patient object being sent to an HttpTrigger Azure Function.

pjirsa_1-1648068763273.png

I want the function to parse this object directly to the Firely.NET Patient class. Some of the properties are successfully hydrated. However, most are not. Even primitive data types are not correctly parsed. No exception is thrown, and the properties are null.

 

So, why is this happening?

 

JSON Serializers and Parsers (Deserializers)

By default, the C# Azure Function is using System.Text.Json.JsonSerializer to parse the incoming request body when the ReadFromJsonAsync() method is used. An alternative, and popular, serializer is Newtonsoft.Json. However, the same behavior is observed.

 

The Firely.NET model classes are complex objects composed of many layers of other FHIR model classes. Therefore, most of them are not able to be properly parsed without customization. To solve this issue, the Firely SDK provides its own set of parsers and serializers to handle the job. So, one option is to just read the body of the incoming request as a string and pass it directly to the Firely parser. This would be a perfectly acceptable solution if my function was only expected to receive FHIR Bundle objects as the full body of the request. But what if my request includes other data as well? Now I would be forced to manually parse the string body or parse it to a generic JObject so I can navigate through the properties. Again, this would be an acceptable solution. But it creates a lot more code and maintenance overhead.

 

Custom Converter

A better solution is to use a custom converter to parse our object. Custom converters can be configured for the default System.Text.Json serializer or Newtonsoft.Json. It inherits from the JsonConverter class and specifies which model class it applies to.

 

Here is a simple example of a custom converter using System.Text.Json to parse a DateTimeOffset value:

 

using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Models.Converters
{
    public class DateTimeOffsetJsonConverter : JsonConverter<DateTimeOffset>
    {
        public override DateTimeOffset Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options) =>
                DateTimeOffset.ParseExact(reader.GetString()!,
                    "MM/dd/yyyy", CultureInfo.InvariantCulture);

        public override void Write(
            Utf8JsonWriter writer,
            DateTimeOffset dateTimeValue,
            JsonSerializerOptions options) =>
                writer.WriteStringValue(dateTimeValue.ToString(
                    "MM/dd/yyyy", CultureInfo.InvariantCulture));
    }
}

 

 

The converter includes two methods that override from the base class:

  • Read() is used to parse the incoming JSON
  • Write() is used to serialize the POCO object back to JSON

 

Learn more about writing custom converters here - How to write custom converters for JSON serialization - .NET | Microsoft Docs

 

Now that we know how to write a converter, let’s create a generic converter, this time for Newtonsoft.JSON, that can handle any of our FHIR model classes that inherit from our FHIR Base class.

 

 

using Hl7.Fhir.ElementModel;
using Hl7.Fhir.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Models.Converters
{
    public class FhirResourceConverter<T> : JsonConverter<T> where T : Hl7.Fhir.Model.Base
    {
        public override T ReadJson(JsonReader reader, Type objectType, T? existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            var valStr = JRaw.Create(reader).ToString();
            return FhirJsonNode.Parse(valStr).ToPoco<T>();
        }

        public override void WriteJson(JsonWriter writer, T? value, JsonSerializer serializer)
        {
            var fhirSerializer = new FhirJsonSerializer();
            writer.WriteRaw(fhirSerializer.SerializeToString(value));
        }
    }
}

 

 

We could have used System.Text.Json. But the Newtonsoft JsonReader makes it a little bit easier for us to work with the raw JSON. To solve our original issue of parsing a FHIR Patient, we simply override the ReadJson() method and use it to call the Firely FhirJsonNode parser.

 

Configuration with Dependency Injection

To make our converter globally available, we must configure the serializer settings in our program with dependency injection. In our program file, we use an extension method to add the configuration to the HostBuilder.

 

 

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults(workerApp =>
    {
        workerApp.UseNewtonsoftJson();
    })
    .Build();

host.Run();

 

 

The extension method is defined using the following static class:

 

 

internal static class WorkerConfigurationExtensions
{
    public static IFunctionsWorkerApplicationBuilder UseNewtonsoftJson(this IFunctionsWorkerApplicationBuilder builder)
    {
        builder.Services.Configure<WorkerOptions>(workerOptions =>
        {
            var settings = NewtonsoftJsonObjectSerializer.CreateJsonSerializerSettings();
            settings.ContractResolver = new CamelCasePropertyNamesContractResolver();
            settings.NullValueHandling = NullValueHandling.Ignore;
            settings.Converters.Add(new FhirResourceConverter<Patient>());

            workerOptions.Serializer = new NewtonsoftJsonObjectSerializer(settings);
        });

        return builder;
    }
}

 

 

In this extension, we add our custom converter for the type of Patient to the collection of converters available to the JsonSerializer. Anytime our code needs to deserialize JSON to a Patient object, our application will know to use our custom converter. You can also provide this same level of dependency injection for custom converters in a standard dotnet API as demonstrated in this blog article.

 

Summary

Problem statement: Sending a FHIR JSON payload to a WebAPI or Azure Function HttpTrigger results in incorrectly parsed POCO models.

Good solution: Read the request payload as a string and manually parse to desired models.

Better solution: Create a custom JsonConverter to handle the parsing logic and configure globally with dependency injection.

 

Resources

1 Comment
Co-Authors
Version history
Last update:
‎Mar 23 2022 02:10 PM
Updated by: