FirelyTeam / firely-net-sdk

The official Firely .NET SDK for HL7 FHIR
Other
823 stars 342 forks source link

Validation does not allow empty Content in Binary (Hl7.Fhir.R5) #2821

Closed andrewrandallcaremetx closed 2 months ago

andrewrandallcaremetx commented 3 months ago

Describe the bug Generating a Bundle, including a Binary wrapped in an Entry, does not pass Validation. The Validation believes that Content should not be null. This requirement was true in R4, but should no longer apply in R5. Deserializing the message works correctly, but bundle.Validate(recurse: true) throws the following exception:

System.ComponentModel.DataAnnotations.ValidationException: Element 'ContentElement' with minimum cardinality 1 cannot be null. At Bundle.Entry.Resource.ContentElement, line , position
         at System.ComponentModel.DataAnnotations.Validator.ValidationError.ThrowValidationException()
         at System.ComponentModel.DataAnnotations.Validator.ValidateObject(Object instance, ValidationContext validationContext, Boolean validateAllProperties)
         at Hl7.Fhir.Validation.DotNetAttributeValidation.Validate(Object value, Boolean recurse, NarrativeValidationKind narrativeValidation)
         at Hl7.Fhir.Validation.DotNetAttributeValidation.Validate(Base value, Boolean recurse, NarrativeValidationKind narrativeValidation)

To Reproduce

using Hl7.Fhir.Model;
using Hl7.Fhir.Serialization;
using Hl7.Fhir.Validation;
using System.Text.Json;
var bundle = new Bundle()
{
    Type = Bundle.BundleType.Message,
    Id = Guid.NewGuid().ToString(),
    Identifier = new Identifier
    {
        System = "example",
        Value = Guid.NewGuid().ToString(),
        Assigner = new ResourceReference
        {
            Display = "example"
        },
        Type = new CodeableConcept
        {
            Coding = [
                new Coding
                {
                    System = "http://terminology.hl7.org/CodeSystem/v2-0203",
                    Code = "RI",
                    Display = "Resource identifier"
                }
            ]
        }
    }
};

var list = new List()
{
    Id = Guid.NewGuid().ToString(),
    Status = List.ListStatus.Current,
    Mode = ListMode.Changes,
    Title = "messageContext"
};

var headerId = Guid.NewGuid().ToString();
bundle.Entry.Add(
    new Bundle.EntryComponent
    {
        FullUrl = $"urn:uuid:{headerId}",
        Resource = new MessageHeader
        {
            Id = headerId,
            Event = new Coding
            {
                System = "example",
                Display = "example"
            },
            Focus = new List<ResourceReference>
            {
                new ResourceReference
                {
                    Reference = $"urn:uuid:{list.Id}",
                    Type = "List"
                }
            },
            Source = new MessageHeader.MessageSourceComponent
            {
                Name = "example"
            }
        }
    });

bundle.Entry.Add(
    new Bundle.EntryComponent
    {
        FullUrl = $"urn:uuid:{list.Id}",
        Resource = list
    });

var binaryId = Guid.NewGuid().ToString();
var binaryEntry = new Bundle.EntryComponent
{
    FullUrl = $"urn:uuid:{binaryId}",
    Resource = new Binary
    {
        IdElement = new Id(binaryId),
        ContentType = "application/pdf"
    }
};
bundle.Entry.Add(binaryEntry);
list.Entry.Add(
    new List.EntryComponent
    {
        Item = new ResourceReference(binaryEntry.Resource.Id)
        {
            Type = binaryEntry.Resource.TypeName
        }
    });

var docRefId = Guid.NewGuid().ToString();
var docRefEntry = new Bundle.EntryComponent
{
    FullUrl = $"urn:uuid:{docRefId}",
    Resource = new DocumentReference
    {
        IdElement = new Id(docRefId),
        Status = DocumentReference.DocumentReferenceStatus.Current,
        Content = new List<DocumentReference.ContentComponent>
        {
            new DocumentReference.ContentComponent
            {
                Attachment = new Attachment
                {
                    Title = "file.pdf",
                    Url = $"s3://file.pdf"
                }
            }
        },
        Subject = new ResourceReference(binaryEntry.FullUrl)
        {
            Type = "Binary"
        }
    }
};
bundle.Entry.Add(docRefEntry);
list.Entry.Add(
    new List.EntryComponent
    {
        Item = new ResourceReference(docRefEntry.Resource.Id)
        {
            Type = docRefEntry.Resource.TypeName
        }
    });

var serializer = new FhirJsonSerializer(SerializerConfigs.FhirLib);
var bundleJson = serializer.SerializeToString(bundle);

var options = new JsonSerializerOptions().ForFhir(ModelInfo.ModelInspector);
var deserializedBundle = JsonSerializer.Deserialize<Bundle>(bundleJson, options);

bundle.Validate(true);

Expected behavior .Validate(recurse: true) should not return any validation error.

Version used:

danieldavis-caremetx commented 3 months ago

It is worth noting that HL7.Fhir.Model.Binary defines the following:

/// <summary>
/// The actual content. Note: Element is replaced by 'Binary.data' since R4. Do not use this element 'content' with R4 and newer releases.
/// </summary>
[FhirElement("content", Order=70)]
[NotMapped(Since=FhirRelease.R4)]
[Cardinality(Min=1,Max=1)]
[DataMember]
public Hl7.Fhir.Model.Base64Binary ContentElement
{
  get { return _ContentElement; }
  set { _ContentElement = value; OnPropertyChanged("ContentElement"); }
}

Though ContentElement has been replaced since R4, Hl7.Fhir.Validation still appears to be validating its Cardinality attribute.

ewoutkramer commented 2 months ago

Yeah, one more side-effect caused by the fact that we use two fields instead of one. See also FirelyTeam/firely-net-sdk#2786.

ewoutkramer commented 2 months ago

Maybe add Since to the Cardinality attribute too? Or is that a hack for the deeper problem that only one of the two properties should be considered for validation (and deserialization) at any moment?