From 4f36d43dfde10ae2aae897f9dcf6d4eb411e2d84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Skowro=C5=84ski?= Date: Mon, 17 Mar 2025 09:33:16 +0100 Subject: [PATCH 1/2] Fallback to dynamic/guessed types on unknown data --- .../BaseFhirJsonPocoDeserializer.cs | 233 ++++++++++++++++-- .../BaseFhirXmlPocoDeserializer.cs | 135 ++++++++-- ...SerializationExcexptionHandlersJsonPoco.cs | 22 +- .../SerializationExcexptionHandlersXmlPoco.cs | 68 ++++- .../FhirJsonDeserializationTests.cs | 141 ++++++++++- 5 files changed, 534 insertions(+), 65 deletions(-) diff --git a/src/Hl7.Fhir.Base/Serialization/BaseFhirJsonPocoDeserializer.cs b/src/Hl7.Fhir.Base/Serialization/BaseFhirJsonPocoDeserializer.cs index e044200de..b8d381ec4 100644 --- a/src/Hl7.Fhir.Base/Serialization/BaseFhirJsonPocoDeserializer.cs +++ b/src/Hl7.Fhir.Base/Serialization/BaseFhirJsonPocoDeserializer.cs @@ -10,6 +10,7 @@ using Hl7.Fhir.Introspection; using Hl7.Fhir.Model; using Hl7.Fhir.Utility; +using Hl7.Fhir.Validation; using System; using System.Collections; using System.Collections.Generic; @@ -137,9 +138,13 @@ public bool TryDeserializeObject(Type targetType, ref Utf8JsonReader reader, [No { if (reader.TokenType != JsonTokenType.StartObject) { - state.Errors.Add(ERR.EXPECTED_START_OF_OBJECT(ref reader, state.Path.GetInstancePath(), reader.TokenType)); - reader.Recover(); // skip to the end of the construct encountered (value or array) - return null; + var result = deserializeUnexpectedJsonValue(state.Path.GetLastPart(), ref reader, state, stayOnLastToken); + + var target = _inspector.FindClassMapping(typeof(DynamicResource))?.Factory() as Resource; + + target!.SetValue("value", result); + + return target; } (ClassMapping? resourceMapping, FhirJsonException? error) = DetermineClassMappingFromInstance(ref reader, _inspector, state.Path); @@ -236,8 +241,10 @@ private void deserializeObjectInto( { if (reader.TokenType != JsonTokenType.StartObject) { - state.Errors.Add(ERR.EXPECTED_START_OF_OBJECT(ref reader, state.Path.GetInstancePath(), reader.TokenType)); - reader.Recover(); // skip to the end of the construct encountered (value or array) + var result = deserializeUnexpectedJsonValue(state.Path.GetLastPart(), ref reader, state, stayOnLastToken); + + target.SetValue("value", result); + return; } @@ -270,8 +277,8 @@ private void deserializeObjectInto( { state.Errors.Add(error); - // try to recover by skipping to the next property. - reader.SkipTo(JsonTokenType.PropertyName); + // no mapping found, make guess for type and insert into overflow + deserializeUnknownPropertiesInto(target, currentPropertyName, ref reader, state, stayOnLastToken); } else { @@ -280,7 +287,7 @@ private void deserializeObjectInto( try { - state.Path.EnterElement(propMapping!.Name, !propMapping.IsCollection ? null : 0, propMapping.IsPrimitive); + state.Path.EnterElement(propMapping!.Name, isEnteringJsonArray(ref reader) ? 0 : null, propMapping.IsPrimitive); deserializePropertyValueInto(target, currentPropertyName, propMapping, propValueMapping!, ref reader, objectParsingState, state); } finally @@ -310,6 +317,142 @@ private void deserializeObjectInto( PocoDeserializationHelper.RunInstanceValidation(target, Settings.Validator, context, state.Errors); } } + + private void deserializeUnknownPropertiesInto( + T target, + string propertyName, + ref Utf8JsonReader reader, + FhirJsonPocoDeserializerState state, + bool stayOnLastToken = false) where T : Base + { + target.TryGetValue(propertyName, out var value); + + var (name, choiceType) = tryDetectChoiceTypeFromName(propertyName); + + // move past property name + reader.Read(); + + object? result; + if(choiceType is not null) + { + state.Path.EnterElement(name, null, choiceType.IsPrimitive); + + result = DeserializeFhirPrimitive(value as PrimitiveType, name, choiceType, null, ref reader, new(), state); + + state.Path.ExitElement(); + } + else + { + state.Path.EnterElement(name, reader.TokenType == JsonTokenType.StartArray ? 0 : null, isJsonPrimitive(ref reader)); + + result = deserializeUnexpectedJsonValue(propertyName, ref reader, state, stayOnLastToken); + + state.Path.ExitElement(); + } + + target.SetValue(name, result); + (string name, ClassMapping? choiceType) tryDetectChoiceTypeFromName(string propertyName) + { + var span = propertyName.AsSpan(); + for(int i = 0; i < span.Length; i++) + { + if (!char.IsUpper(span[i])) + continue; + + var subSpan = span.Slice(i); + if (subSpan.IsEmpty) + break; + + var choiceMapping = _inspector.FindClassMapping(subSpan.ToString()); + if (choiceMapping is not null) + return (span[..i].ToString(), choiceMapping); + } + return (propertyName, null); + } + } + + private object? deserializeUnexpectedJsonValue(string propertyName, ref Utf8JsonReader reader, FhirJsonPocoDeserializerState state, bool stayOnLastToken, ClassMapping? propertySuggestion = null) + { + if(reader.TokenType == JsonTokenType.StartObject) + { + var propMapping = propertySuggestion ?? _inspector.FindClassMapping(typeof(DynamicDataType))!; + + var primitive = (propMapping.Factory() as Base)!; + + deserializeObjectInto(primitive, propMapping, ref reader, DeserializedObjectKind.FhirPrimitive, state, stayOnLastToken); + + return primitive; + } + else if (reader.TokenType == JsonTokenType.StartArray) + { + var primitiveType = guessFhirPrimitiveType(peekType(ref reader)); + + var propMapping = propertySuggestion ?? _inspector.FindClassMapping(primitiveType!)!; + + var primitiveList = propMapping.ListFactory(); + + var objectState = new ObjectParsingState(); + + deserializeFhirPrimitiveList(primitiveList, propertyName, propMapping, primitiveType, ref reader, objectState, state); + + return primitiveList; + } + else if (reader.TokenType switch + { + JsonTokenType.String => typeof(FhirString), + JsonTokenType.Number => typeof(FhirDecimal), + JsonTokenType.True or JsonTokenType.False => typeof(FhirBoolean), + _ => null + } is { } type) + { + var (primitive, error) = DeserializePrimitiveValue(ref reader, type, state.Path); + + if(error is not null) + state.Errors.Add(error); + + return new DynamicPrimitive { ObjectValue = primitive }; + } + + return null; + } + + private static JsonTokenType peekType(ref Utf8JsonReader reader) + { + if (reader.TokenType == JsonTokenType.StartArray) + { + var peekCopy = reader; + + peekCopy.Read(); + + return peekCopy.TokenType; + } + + return reader.TokenType; + } + + private static Type? guessFhirPrimitiveType(JsonTokenType tokenType) + { + return tokenType switch + { + JsonTokenType.String => typeof(FhirString), + JsonTokenType.Number => typeof(FhirDecimal), + JsonTokenType.True or JsonTokenType.False => typeof(FhirBoolean), + JsonTokenType.StartObject => typeof(DynamicPrimitive), + _ => null + }; + } + + private static bool isEnteringJsonArray(ref Utf8JsonReader reader) + { + return reader.TokenType == JsonTokenType.StartArray; + } + + private static bool isJsonPrimitive(ref Utf8JsonReader reader) + { + return reader.TokenType + is JsonTokenType.String or JsonTokenType.Number + or JsonTokenType.False or JsonTokenType.True; + } /// /// Reads the value of a json property. @@ -342,7 +485,7 @@ FhirJsonPocoDeserializerState state // There might be an existing value, since FhirPrimitives may be spread out over two properties // (one with, and one without the '_') - var existingValue = propertyMapping.GetValue(target); + target.TryGetValue(propertyMapping.Name, out var existingValue); if (propertyValueMapping.IsFhirPrimitive) { @@ -353,8 +496,8 @@ FhirJsonPocoDeserializerState state // Note that the POCO model will always allocate a new list if the property had not been set before, // so there is always an existingValue for IList - result = propertyMapping.IsCollection ? - deserializeFhirPrimitiveList((IList)existingValue!, propertyName, propertyValueMapping, fhirType, ref reader, delayedValidations, state) : + result = isEnteringJsonArray(ref reader) ? + deserializeFhirPrimitiveList((IList)(existingValue ?? propertyValueMapping.ListFactory()), propertyName, propertyValueMapping, fhirType, ref reader, delayedValidations, state) : DeserializeFhirPrimitive(existingValue as PrimitiveType, propertyName, propertyValueMapping, fhirType, ref reader, delayedValidations, state); } else @@ -363,10 +506,15 @@ FhirJsonPocoDeserializerState state if (propertyName[0] == '_') state.Errors.Add(ERR.USE_OF_UNDERSCORE_ILLEGAL(ref reader, state.Path.GetInstancePath(), propertyMapping.Name, propertyName)); + // handle case where we detect array where it shouldn't be, or primitive where there should be array + if (isEnteringJsonArray(ref reader) != propertyMapping.IsCollection) + { + result = deserializeUnexpectedJsonValue(propertyName, ref reader, state, false, propertyValueMapping); + } // Note that repeating simple elements (like Extension.url) do not currently exist in the FHIR serialization - if (propertyMapping.IsCollection) + else if(propertyMapping.IsCollection) { - var l = (IList)existingValue!; + var l = (IList)(existingValue ?? propertyValueMapping.ListFactory()); // if the list is already populated, a property with an identical key was encountered earlier if (l.Count > 0) { @@ -419,7 +567,15 @@ FhirJsonPocoDeserializerState state PocoDeserializationHelper.RunPropertyValidation(result, Settings.Validator!, deserializationContext, state.Errors); } - propertyMapping.SetValue(target, result); + target.SetValue(propertyMapping.Name, result); + try + { + _ = propertyMapping.GetValue(target); + } + catch (CodedValidationException ex) + { + state.Errors.Add(new CodedValidationException(ex.ErrorCode, ex.Message, state.Path.GetInstancePath(), line, pos, ex.IssueSeverity, ex.IssueType)); + } } /// @@ -617,7 +773,16 @@ FhirJsonPocoDeserializerState state { state.Path.EnterElement("value", 0, true); - var (result, error) = DeserializePrimitiveValue(ref reader, primitiveValueProperty.ImplementingType, state.Path); + object? result; + FhirJsonException? error = null; + if (reader.TokenType is JsonTokenType.StartObject or JsonTokenType.StartArray) + { + result = deserializeUnexpectedJsonValue(propertyName, ref reader, state, false); + } + else + { + (result, error) = DeserializePrimitiveValue(ref reader, primitiveValueProperty.ImplementingType, state.Path); + } if (error is not null) state.Errors.Add(error); @@ -767,13 +932,21 @@ internal static (ClassMapping?, FhirJsonException?) DetermineClassMappingFromIns { var (resourceType, error) = determineResourceType(ref reader); - if (resourceType is null) return (null, error); - - var resourceMapping = inspector.FindClassMapping(resourceType); - - return resourceMapping is not null ? - (new(resourceMapping, null)) : - (new(null, ERR.UNKNOWN_RESOURCE_TYPE(ref reader, path.GetInstancePath(), resourceType))); + ClassMapping? resourceMapping = null; + if (resourceType is not null) + resourceMapping = inspector.FindClassMapping(resourceType); + + // fall back to DynamicResource, if we can't find the resource requested + resourceMapping ??= inspector.FindClassMapping(nameof(DynamicResource)); + + if(resourceMapping is not null) + return (resourceMapping, null); + + if(resourceType is null) + return (null, error); + + + return (null, ERR.UNKNOWN_RESOURCE_TYPE(ref reader, path.GetInstancePath(), resourceType)); } private static (string?, FhirJsonException?) determineResourceType(ref Utf8JsonReader reader) @@ -830,6 +1003,7 @@ private static (PropertyMapping? propMapping, ClassMapping? propValueMapping, Fh var propertyMapping = parentMapping.FindMappedElementByName(elementName) ?? parentMapping.FindMappedElementByChoiceName(elementName); + // handled by the unknown type deserialization if (propertyMapping is null) return (null, null, ERR.UNKNOWN_PROPERTY_FOUND(ref reader, path.GetInstancePath(), propertyName)); @@ -849,11 +1023,16 @@ private static (PropertyMapping? propMapping, ClassMapping? propValueMapping, Fh { string typeSuffix = elementName[propertyMapping.Name.Length..]; - return string.IsNullOrEmpty(typeSuffix) - ? (null, ERR.CHOICE_ELEMENT_HAS_NO_TYPE(ref r, path.GetInstancePath(), propertyMapping.Name)) - : inspector.FindClassMapping(typeSuffix) is { } cm - ? (cm, null) - : (null, ERR.CHOICE_ELEMENT_HAS_UNKOWN_TYPE(ref r, path.GetInstancePath(), propertyMapping.Name, typeSuffix)); + ClassMapping? choiceMapping = null; + if(!string.IsNullOrEmpty(typeSuffix)) + choiceMapping = inspector.FindClassMapping(typeSuffix); + + choiceMapping ??= inspector.FindClassMapping(nameof(DynamicDataType)); + + if(choiceMapping is not null) + return (choiceMapping, null); + + return (null, ERR.CHOICE_ELEMENT_HAS_UNKOWN_TYPE(ref r, path.GetInstancePath(), propertyMapping.Name, typeSuffix)); } } } diff --git a/src/Hl7.Fhir.Base/Serialization/BaseFhirXmlPocoDeserializer.cs b/src/Hl7.Fhir.Base/Serialization/BaseFhirXmlPocoDeserializer.cs index a52722383..6f0b9605a 100644 --- a/src/Hl7.Fhir.Base/Serialization/BaseFhirXmlPocoDeserializer.cs +++ b/src/Hl7.Fhir.Base/Serialization/BaseFhirXmlPocoDeserializer.cs @@ -3,6 +3,7 @@ using Hl7.Fhir.Introspection; using Hl7.Fhir.Model; using Hl7.Fhir.Utility; +using Hl7.Fhir.Validation; using System; using System.Collections; using System.Collections.Generic; @@ -233,7 +234,7 @@ internal void DeserializeElementInto(Base target, ClassMapping mapping, XmlReade throw new InvalidOperationException($"Xml node of type '{reader.NodeType}' is unexpected at this point"); if (reader.HasAttributes) - readAttributes(target, mapping, reader, state); + readAttributes(_inspector, target, mapping, reader, state); //Empty elements have no children e.g. getChoiceClassMapping(reader), @@ -589,13 +689,18 @@ private static (PropertyMapping? propMapping, ClassMapping? propValueMapping, Fh (ClassMapping?, FhirXmlException?) getChoiceClassMapping(XmlReader r) { + ClassMapping? choiceMapping = null; string typeSuffix = propertyName[propertyMapping.Name.Length..]; - - return string.IsNullOrEmpty(typeSuffix) - ? (null, ERR.CHOICE_ELEMENT_HAS_NO_TYPE(r, path.GetInstancePath(), propertyMapping.Name)) - : inspector.FindClassMapping(typeSuffix) is { } cm - ? (cm, null) - : (null, ERR.CHOICE_ELEMENT_HAS_UNKOWN_TYPE(r, path.GetInstancePath(), propertyMapping.Name, typeSuffix)); + + if(!string.IsNullOrEmpty(typeSuffix)) + choiceMapping = inspector.FindClassMapping(typeSuffix); + + choiceMapping ??= inspector.FindClassMapping(nameof(DynamicDataType)); + + if(choiceMapping is not null) + return (choiceMapping, null); + + return (null, ERR.CHOICE_ELEMENT_HAS_UNKOWN_TYPE(r, path.GetInstancePath(), propertyMapping.Name, typeSuffix)); } } } diff --git a/src/Hl7.Fhir.Serialization.Shared.Tests/SerializationExcexptionHandlersJsonPoco.cs b/src/Hl7.Fhir.Serialization.Shared.Tests/SerializationExcexptionHandlersJsonPoco.cs index ff7e7f656..e745c12a1 100644 --- a/src/Hl7.Fhir.Serialization.Shared.Tests/SerializationExcexptionHandlersJsonPoco.cs +++ b/src/Hl7.Fhir.Serialization.Shared.Tests/SerializationExcexptionHandlersJsonPoco.cs @@ -613,9 +613,9 @@ public void JsonInvalidExtensionNonArray() DebugDump.OutputXml(oc); DebugDump.OutputJson(ex.PartialResult); - Assert.AreEqual("Patient.birthDate.extension[0]", oc.Issue[0].Expression.First()); + Assert.AreEqual("Patient.birthDate.extension", oc.Issue[0].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[0].Severity); - Assert.AreEqual(FhirJsonException.EXPECTED_START_OF_ARRAY_CODE, oc.Issue[0].Details.Coding[0].Code); + Assert.AreEqual(CodedValidationException.EXPECTED_ARRAY_NOT_OBJECT_CODE, oc.Issue[0].Details.Coding[0].Code); Assert.AreEqual(1, oc.Issue.Count); } @@ -645,7 +645,7 @@ public void JsonInvalidExtensionNonObjectInArray() { var p = serializeResource(rawData); DebugDump.OutputJson(p); - Assert.Fail("Expected to throw parsing"); + // Assert.Fail("Expected to throw parsing"); } catch (DeserializationFailedException ex) { @@ -781,19 +781,15 @@ public void JsonInvalidElementIdArrayPath() DebugDump.OutputXml(oc); DebugDump.OutputJson(ex.PartialResult); - Assert.AreEqual(FhirJsonException.EXPECTED_START_OF_OBJECT_CODE, oc.Issue[0].Details.Coding[0].Code); - Assert.AreEqual(OperationOutcome.IssueSeverity.Fatal, oc.Issue[0].Severity); - Assert.AreEqual("Patient.name[0].given[1].extension[1]", oc.Issue[0].Expression.First()); - - Assert.AreEqual(COVE.MANDATORY_ELEMENT_CANNOT_BE_NULL_CODE, oc.Issue[1].Details.Coding[0].Code); + Assert.AreEqual(COVE.MANDATORY_ELEMENT_CANNOT_BE_NULL_CODE, oc.Issue[0].Details.Coding[0].Code); Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[1].Severity); - Assert.AreEqual("Patient.name[0].given[1].extension[2].url", oc.Issue[1].Expression.First()); + Assert.AreEqual("Patient.name[0].given[1].extension[2].url", oc.Issue[0].Expression.First()); - Assert.AreEqual(COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE, oc.Issue[2].Details.Coding[0].Code); - Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[2].Severity); - Assert.AreEqual("Patient.name[0].given[1].id", oc.Issue[2].Expression.First()); + Assert.AreEqual(COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE, oc.Issue[1].Details.Coding[0].Code); + Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[1].Severity); + Assert.AreEqual("Patient.name[0].given[1].id", oc.Issue[1].Expression.First()); - Assert.AreEqual(3, oc.Issue.Count); + Assert.AreEqual(2, oc.Issue.Count); } } diff --git a/src/Hl7.Fhir.Serialization.Shared.Tests/SerializationExcexptionHandlersXmlPoco.cs b/src/Hl7.Fhir.Serialization.Shared.Tests/SerializationExcexptionHandlersXmlPoco.cs index 65d0ed296..72a467a2f 100644 --- a/src/Hl7.Fhir.Serialization.Shared.Tests/SerializationExcexptionHandlersXmlPoco.cs +++ b/src/Hl7.Fhir.Serialization.Shared.Tests/SerializationExcexptionHandlersXmlPoco.cs @@ -109,11 +109,11 @@ public void XMLInvalidMultipleSinglePropValues() Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[0].Severity); Assert.AreEqual("XML121", oc.Issue[0].Details.Coding[0].Code); - Assert.AreEqual("Patient.contact[0].gender", oc.Issue[1].Expression.First()); - Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[1].Severity); - Assert.AreEqual("PVAL116", oc.Issue[1].Details.Coding[0].Code); + Assert.AreEqual("Patient.contact[0].gender", oc.Issue[2].Expression.First()); + Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[2].Severity); + Assert.AreEqual("PVAL116", oc.Issue[2].Details.Coding[0].Code); - Assert.AreEqual(2, oc.Issue.Count); + Assert.AreEqual(3, oc.Issue.Count); } } @@ -250,6 +250,50 @@ public void XMLInvalidBooleanValue() Assert.AreEqual(2, oc.Issue.Count); } } + + [TestMethod] + public void XMLInvalidRepeatingOnNonRepeating() + { + // string containing a FHIR Patient with name John Doe, 17 Jan 1970, an invalid gender and an invalid date of birth + string rawData = """ + + + + + + + + + + """; + try + { + var p = SerializeResource(rawData); + DebugDump.OutputXml(p); + Assert.Fail("Expected to throw parsing"); + } + catch (DeserializationFailedException ex) + { + System.Diagnostics.Trace.WriteLine($"{ex.Message}"); + OperationOutcome oc = ex.ToOperationOutcome(); + DebugDump.OutputXml(oc); + DebugDump.OutputXml(ex.PartialResult); + + Assert.AreEqual("Patient.active", oc.Issue[0].Expression.First()); + Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[0].Severity); + Assert.AreEqual("XML121", oc.Issue[0].Details.Coding[0].Code); + + Assert.AreEqual("Patient.active", oc.Issue[1].Expression.First()); + Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[1].Severity); + Assert.AreEqual(COVE.EXPECTED_PRIMITIVE_NOT_ARRAY_CODE, oc.Issue[1].Details.Coding[0].Code); + + Assert.AreEqual("Patient.birthDate", oc.Issue[2].Expression.First()); + Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[2].Severity); + Assert.AreEqual(COVE.LITERAL_INVALID_CODE, oc.Issue[2].Details.Coding[0].Code); + + Assert.AreEqual(3, oc.Issue.Count); + } + } [TestMethod] public void XMLInvalidDateValueWithTime() @@ -328,7 +372,7 @@ public void XMLInvalidPropertyOrdering() } [TestMethod] - public void XMLInvalidPropertyDetected() + public void XMLInvalidPropertySupportedWithOverflow() { string rawData = """ @@ -370,11 +414,15 @@ public void XMLInvalidPropertyDetected() Assert.AreEqual(OperationOutcome.IssueSeverity.Fatal, oc.Issue[1].Severity); Assert.AreEqual("XML104", oc.Issue[1].Details.Coding[0].Code); - Assert.AreEqual("Patient", oc.Issue[2].Expression.First()); - Assert.AreEqual(OperationOutcome.IssueSeverity.Fatal, oc.Issue[2].Severity); - Assert.AreEqual("XML104", oc.Issue[2].Details.Coding[0].Code); + Assert.AreEqual("Patient.name[1]", oc.Issue[2].Expression.First()); + Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[2].Severity); + Assert.AreEqual("XML120", oc.Issue[2].Details.Coding[0].Code); - Assert.AreEqual(3, oc.Issue.Count); + Assert.AreEqual("Patient", oc.Issue[3].Expression.First()); + Assert.AreEqual(OperationOutcome.IssueSeverity.Fatal, oc.Issue[3].Severity); + Assert.AreEqual("XML104", oc.Issue[3].Details.Coding[0].Code); + + Assert.AreEqual(4, oc.Issue.Count); } } @@ -507,6 +555,8 @@ public void XMLMixedInvalidParseIssues() OperationOutcome oc = ex.ToOperationOutcome(); DebugDump.OutputXml(oc); DebugDump.OutputXml(ex.PartialResult); + + Assert.AreEqual(8, oc.Issue.Count); } } diff --git a/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirJsonDeserializationTests.cs b/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirJsonDeserializationTests.cs index 65f7d0887..266d9d01e 100644 --- a/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirJsonDeserializationTests.cs +++ b/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirJsonDeserializationTests.cs @@ -37,7 +37,6 @@ public void PrimitiveValueCannotBeComplex() [DataTestMethod] [DataRow("OperationOutcome", null)] - [DataRow("OperationOutcomeX", ERR.UNKNOWN_RESOURCE_TYPE_CODE)] [DataRow("Meta", null)] [DataRow(4, ERR.RESOURCETYPE_SHOULD_BE_STRING_CODE)] [DataRow(null, ERR.NO_RESOURCETYPE_PROPERTY_CODE)] @@ -571,6 +570,146 @@ static Attachment deserializeAttachment(FhirJsonConverterOptions settings) } } + [TestMethod] + public void JsonDeserializerSupportsParsingUnknownTypesAndProperties() + { + var parser = new BaseFhirJsonPocoDeserializer(ModelInspector.Base); + + var dt = DateTimeOffset.UtcNow; + + Utf8JsonReader reader = constructReader(new { resourceType = "Unknown", id = "TestIdentifier", body = new[] { "Test" }, testBool = true, valueDateTime = dt, testDec = 123.4, testInt = 999}); + + parser.TryDeserializeResource(ref reader, out var obj, out var errors); + + obj.Should().NotBeNull(); + obj!.Id.Should().Be("TestIdentifier"); + obj["body"].Should().BeEquivalentTo(new List { new("Test") }); + obj["testBool"].Should().BeEquivalentTo(new DynamicPrimitive{ ObjectValue = true }); + obj["testDec"].Should().BeEquivalentTo(new DynamicPrimitive{ ObjectValue = new decimal(123.4) }); + obj["testInt"].Should().BeEquivalentTo(new DynamicPrimitive{ ObjectValue = 999}); + obj["value"].Should().BeEquivalentTo(new FhirDateTime(dt)); + } + + [TestMethod] + public void JsonDeserializerSupportsUnknownPropertiesOnKnownTypes() + { + var parser = new BaseFhirJsonPocoDeserializer(ModelInspector.ForType()); + + var dt = DateTimeOffset.UtcNow; + + Utf8JsonReader reader = constructReader(new + { + resourceType = "Patient", + id = "TestIdentifier", + active = new[] { true, false }, + telecom = new{ system = "phone", value = "magicnumber"}, + communication = "en", + name = "Test", + }); + + parser.TryDeserializeResource(ref reader, out var obj, out var errors); + var serial = new BaseFhirJsonSerializer(ModelInspector.ForType()); + + obj.Should().NotBeNull(); + obj!.TypeName.Should().Be("Patient"); + obj.Id.Should().Be("TestIdentifier"); + // array where primitive + obj["active"].Should().BeEquivalentTo(new[]{new FhirBoolean(true), new FhirBoolean(false)}); + // primitive where array + obj["communication"].Should().BeEquivalentTo(new DynamicPrimitive{ ObjectValue = "en" }); + // primitive when complex + obj["name"].Should().BeEquivalentTo(new DynamicPrimitive{ ObjectValue = "Test"}); + } + + [TestMethod] + public void JsonDeserializerHandleUnexpectedChoiceType() + { + var parser = new BaseFhirJsonPocoDeserializer(ModelInspector.ForType()); + + var dt = DateTimeOffset.UtcNow; + + // var test = new + // { + // resourceType = "Observation", + // id = "obs-001", + // valueQuantity = new + // { + // value = 98.6, + // unit = "°F", + // system = "http://unitsofmeasure.org", + // code = "degF" + // }, + // valueString = "Normal" + // }; + + var test = new + { + resourceType = "Observation", + status = new { value = "final" }, // Expected a primitive, got an object + code = new + { + text = "Heart Rate" + }, + valueQuantity = new + { + value = new { amount = 72 }, // Expected a number, got an object + unit = "bpm" + } + }; + + + Utf8JsonReader reader = constructReader(test); + + parser.TryDeserializeResource(ref reader, out var obj, out var errors); + + obj.Should().NotBeNull(); + obj!.TypeName.Should().Be("Observation"); + // errors.Should().ContainSingle(x => x.Message.Contains("Duplicate")); + } + + [TestMethod] + public void JsonDeserializerHandleContainedStuff() + { + var parser = new BaseFhirJsonPocoDeserializer(ModelInspector.ForType()); + + var dt = DateTimeOffset.UtcNow; + + // var test = new + // { + // resourceType = "Observation", + // id = "obs-001", + // valueQuantity = new + // { + // value = 98.6, + // unit = "°F", + // system = "http://unitsofmeasure.org", + // code = "degF" + // }, + // valueString = "Normal" + // }; + + var test = new + { + resourceType = "Patient", + id = "patient", + name = new []{ new { Family = "Doe", Given = new[] { "John" } } }, + contained = new[] + { + new { resourceType = "Medication", id = "medication", code = "1234" } + } + }; + + Utf8JsonReader reader = constructReader(test); + + parser.TryDeserializeResource(ref reader, out var obj, out var errors); + + obj.Should().NotBeNull(); + obj!.TypeName.Should().Be("Patient"); + (obj as Patient)!.Contained.Should().HaveCount(1).And.Subject.Should().Satisfy(x => x.TypeName == "Medication"); + // errors.Should().ContainSingle(x => x.Message.Contains("Duplicate")); + } + + internal class CustomComplexValidator : DataAnnotationDeserialzationValidator { //public object? DateTimeSeenByObjectValueValidator; From baa5f3dfdff1a574e0fdc21d5fe7a380649a86c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Skowro=C5=84ski?= Date: Mon, 17 Mar 2025 09:42:44 +0100 Subject: [PATCH 2/2] Cleanup --- .../FhirJsonDeserializationTests.cs | 37 +------------------ 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirJsonDeserializationTests.cs b/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirJsonDeserializationTests.cs index 266d9d01e..aa18294ef 100644 --- a/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirJsonDeserializationTests.cs +++ b/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirJsonDeserializationTests.cs @@ -622,26 +622,10 @@ public void JsonDeserializerSupportsUnknownPropertiesOnKnownTypes() } [TestMethod] - public void JsonDeserializerHandleUnexpectedChoiceType() + public void JsonDeserializerHandleUnexpectedObject() { var parser = new BaseFhirJsonPocoDeserializer(ModelInspector.ForType()); - var dt = DateTimeOffset.UtcNow; - - // var test = new - // { - // resourceType = "Observation", - // id = "obs-001", - // valueQuantity = new - // { - // value = 98.6, - // unit = "°F", - // system = "http://unitsofmeasure.org", - // code = "degF" - // }, - // valueString = "Normal" - // }; - var test = new { resourceType = "Observation", @@ -657,14 +641,12 @@ public void JsonDeserializerHandleUnexpectedChoiceType() } }; - Utf8JsonReader reader = constructReader(test); parser.TryDeserializeResource(ref reader, out var obj, out var errors); obj.Should().NotBeNull(); obj!.TypeName.Should().Be("Observation"); - // errors.Should().ContainSingle(x => x.Message.Contains("Duplicate")); } [TestMethod] @@ -672,22 +654,6 @@ public void JsonDeserializerHandleContainedStuff() { var parser = new BaseFhirJsonPocoDeserializer(ModelInspector.ForType()); - var dt = DateTimeOffset.UtcNow; - - // var test = new - // { - // resourceType = "Observation", - // id = "obs-001", - // valueQuantity = new - // { - // value = 98.6, - // unit = "°F", - // system = "http://unitsofmeasure.org", - // code = "degF" - // }, - // valueString = "Normal" - // }; - var test = new { resourceType = "Patient", @@ -706,7 +672,6 @@ public void JsonDeserializerHandleContainedStuff() obj.Should().NotBeNull(); obj!.TypeName.Should().Be("Patient"); (obj as Patient)!.Contained.Should().HaveCount(1).And.Subject.Should().Satisfy(x => x.TypeName == "Medication"); - // errors.Should().ContainSingle(x => x.Message.Contains("Duplicate")); }