firely-net-sdk icon indicating copy to clipboard operation
firely-net-sdk copied to clipboard

FHIR JSON Serializer Problems with a custom resource

Open ewoutkramer opened this issue 1 year ago • 0 comments

Discussed in https://github.com/FirelyTeam/firely-net-sdk/discussions/2579

Originally posted by Sneedd September 1, 2023 Help! Been stuck a while now. Tried different approaches to make it work, but so far nothing worked.

Basically I am trying to serialize and deserialize an custom resource. Tried the System.Text.Json and Newtonsoft approach.

Here is my code, if you like to try it out: Create a new MSTest .NET6 project, add the Hl7.Fhir.R4 (v5.3.0) library and paste the following code. I added the results over the tests, where 5 of 8 are failing.

// Hl7.Fhir.R4 5.3.0
using Hl7.Fhir.Introspection;
using Hl7.Fhir.Model;
using Hl7.Fhir.Serialization;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Text.Json;


namespace Example
{
    public class Workspace
    {
        public string Id { get; set; }
        public string? Name { get; set; }
        public bool? Active { get; set; }
    }

    [Serializable]
    [DataContract]
    [FhirType("FhirWorkspace", "http://hl7.org/fhir/StructureDefinition/FhirWorkspace", IsResource = true)]
    public class FhirWorkspace : DomainResource
    {
        private readonly Workspace _original;

        [IgnoreDataMember]
        public Workspace Original => _original;

        public override string TypeName => nameof(FhirWorkspace);

        public override IEnumerable<ElementValue> NamedChildren
        {
            get
            {
                foreach (var elementPair in base.NamedChildren)
                {
                    yield return elementPair;
                }
                if (Name != null)
                {
                    yield return new ElementValue("name", Name);
                }
                if (Active != null)
                {
                    yield return new ElementValue("active", Active);
                }
            }
        }

        [DataMember]
        [FhirElement("name", Order = 90)]
        public FhirString Name
        {
            get => new FhirString(_original.Name);
            set => _original.Name = value?.Value;
        }

        [DataMember]
        [FhirElement("active", Order = 100)]
        public FhirBoolean Active
        {
            get => new FhirBoolean(_original.Active);
            set => _original.Active = value?.Value;
        }

        public FhirWorkspace()
            : this(new Workspace())
        {
        }

        public FhirWorkspace(Workspace original)
        {
            _original = original;
        }

        public override IDeepCopyable DeepCopy()
        {
            return new FhirWorkspace(new Workspace { Name = _original.Name });
        }

        protected override IEnumerable<KeyValuePair<string, object>> GetElementPairs()
        {
            foreach (var elementPair in base.GetElementPairs())
            {
                yield return elementPair;
            }
            if (Name != null)
            {
                yield return new KeyValuePair<string, object>("name", Name);
            }
            if (Active != null)
            {
                yield return new KeyValuePair<string, object>("active", Active);
            }
        }
    }

    [TestClass]
    public class FhirSerialization
    {
        private void TestJson1SerializerWith(Workspace workspace)
        {
            var serializer = new FhirJsonSerializer();
            var jsonContent = serializer.SerializeToString(new FhirWorkspace(workspace));

            var parser = new FhirJsonParser();
            var fhirWorkspace = parser.Parse<FhirWorkspace>(jsonContent);
            Assert.IsNotNull(fhirWorkspace);
            var workspace2 = fhirWorkspace.Original;

            Assert.AreEqual(workspace.Name, workspace2.Name);
            Assert.AreEqual(workspace.Active, workspace2.Active);
        }

        // Fails: System.FormatException: While building a POCO: Element 'name' must not repeat (at FhirWorkspace.name[0])
        [TestMethod]
        public void SerializerJson1Test()
        {
            TestJson1SerializerWith(new Workspace { Name = "ABC", Active = true });
        }

        // Fails: System.FormatException: While building a POCO: Element 'active' must not repeat (at FhirWorkspace.active[0])
        [TestMethod]
        public void SerializerJson1_EmptyString_Test()
        {
            TestJson1SerializerWith(new Workspace { Name = "", Active = true });
        }

        // Fails: System.FormatException: While building a POCO: Element 'active' must not repeat (at FhirWorkspace.active[0])
        [TestMethod]
        public void SerializerJson1_NullString_Test()
        {
            TestJson1SerializerWith(new Workspace { Name = null, Active = true });
        }

        // Ok
        [TestMethod]
        public void SerializerJson1_AllNull_Test()
        {
            TestJson1SerializerWith(new Workspace { Name = null, Active = null });
        }

        private void TestJson2SerializerWith(Workspace workspace)
        {
            var modelInspector = new ModelInspector(Hl7.Fhir.Specification.FhirRelease.R4);
            modelInspector.ImportType(typeof(FhirWorkspace));

            var options = new JsonSerializerOptions()
                .ForFhir(modelInspector);

            var jsonContent = JsonSerializer.Serialize(new FhirWorkspace(workspace), typeof(FhirWorkspace), options);

            var fhirWorkspace = JsonSerializer.Deserialize(jsonContent, typeof(FhirWorkspace), options) as FhirWorkspace;
            Assert.IsNotNull(fhirWorkspace);
            var workspace2 = fhirWorkspace.Original;

            Assert.AreEqual(workspace.Name, workspace2.Name);
            Assert.AreEqual(workspace.Active, workspace2.Active);
        }

        // OK
        [TestMethod]
        public void SerializerJson2Test()
        {
            TestJson2SerializerWith(new Workspace { Name = "ABC", Active = true });
        }

        // Fails: Properties cannot be empty strings
        [TestMethod]
        public void SerializerJson2_EmptyString_Test()
        {
            TestJson2SerializerWith(new Workspace { Name = "", Active = true });
        }

        // OK
        [TestMethod]
        public void SerializerJson2_NullString_Test()
        {
            TestJson2SerializerWith(new Workspace { Name = null, Active = true });
        }

        // Fails: An object needs to have at least one property
        [TestMethod]
        public void SerializerJson2_AllNull_Test()
        {
            TestJson2SerializerWith(new Workspace { Name = null, Active = null });
        }
    }
}

So my problems are:

  • Should not the two serializers behave the same?
  • I do not understand the "Element 'xx' must not repeat" messages.
  • Is it really not allowed to have an entity without any property value?
  • Why empty strings are a problem?
  • What I am doing wrong?

ewoutkramer avatar Jan 10 '24 13:01 ewoutkramer