Skip to content

Commit

Permalink
Merge pull request #2455 from FirelyTeam/feature/BP-2454-JsonDupArray
Browse files Browse the repository at this point in the history
#2454 Handle the parsing of json arrays to handle duplicate array values
  • Loading branch information
mmsmits authored May 26, 2023
2 parents 0ca7da5 + d43622e commit f8803ad
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 12 deletions.
47 changes: 35 additions & 12 deletions src/Hl7.Fhir.Base/Serialization/BaseFhirJsonPocoDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ private void deserializeObjectInto<T>(
reader.Read();

var empty = true;
var delayedValidations = new DelayedValidations();
var objectParsingState = new ObjectParsingState();
var oldErrorCount = state.Errors.Count;
var (line, pos) = reader.GetLocation();

Expand Down Expand Up @@ -284,7 +284,7 @@ private void deserializeObjectInto<T>(
try
{
state.Path.EnterElement(propMapping!.Name, !propMapping.IsCollection ? null : 0, propMapping.IsPrimitive);
deserializePropertyValueInto(target, currentPropertyName, propMapping, propValueMapping!, ref reader, delayedValidations, state);
deserializePropertyValueInto(target, currentPropertyName, propMapping, propValueMapping!, ref reader, objectParsingState, state);
}
finally
{
Expand All @@ -297,7 +297,7 @@ private void deserializeObjectInto<T>(
// postponed until after all properties have been seen (e.g. Instance and Property validations for
// primitive properties, since they may be composed from two properties `name` and `_name` in json
// and should only be validated when both have been processed, even if megabytes apart in the json file).
delayedValidations.Run();
objectParsingState.RunDelayedValidation();

// read past object, unless this is the last EndObject in the top-level Deserialize call
if (!stayOnLastToken) reader.Read();
Expand Down Expand Up @@ -337,7 +337,7 @@ private void deserializePropertyValueInto(
PropertyMapping propertyMapping,
ClassMapping propertyValueMapping,
ref Utf8JsonReader reader,
DelayedValidations delayedValidations,
ObjectParsingState delayedValidations,
FhirJsonPocoDeserializerState state
)
{
Expand Down Expand Up @@ -385,7 +385,7 @@ FhirJsonPocoDeserializerState state
// chance to encounter both the `name` and `_name` property.
if (delayedValidations is not null && propertyValueMapping.IsFhirPrimitive)
{
delayedValidations.Schedule(
delayedValidations.ScheduleDelayedValidation(
propertyMapping.Name + PROPERTY_VALIDATION_KEY_SUFFIX,
() => PocoDeserializationHelper.RunPropertyValidation(ref result, Settings.Validator!, deserializationContext, state.Errors));
}
Expand Down Expand Up @@ -454,19 +454,33 @@ FhirJsonPocoDeserializerState state
return listInstance;
}

internal class DelayedValidations
internal class ObjectParsingState
{
private readonly Dictionary<string, Action> _validations = new();
private readonly Dictionary<string, int> _parsedPropValue = new();

public void Schedule(string key, Action validation)
public int GetPropertyIndex(string memberName)
{
if (_parsedPropValue.ContainsKey(memberName))
return _parsedPropValue[memberName];
_parsedPropValue.Add(memberName, 0);
return 0;
}

public void SetPropertyIndex(string memberName, int count)
{
_parsedPropValue[memberName] = count;
}

public void ScheduleDelayedValidation(string key, Action validation)
{
// Add or overwrite the entry for the given key.
if (_validations.ContainsKey(key)) _validations.Remove(key);
_validations[key] = validation;
}

//public CodedValidationException[] Run() => _validations.Values.SelectMany(delayed => delayed()).ToArray();
public void Run()
public void RunDelayedValidation()
{
foreach (var validation in _validations.Values) validation();
}
Expand All @@ -482,7 +496,7 @@ public void Run()
ClassMapping propertyValueMapping,
Type? fhirType,
ref Utf8JsonReader reader,
DelayedValidations delayedValidations,
ObjectParsingState delayedValidations,
FhirJsonPocoDeserializerState state
)
{
Expand Down Expand Up @@ -510,6 +524,12 @@ FhirJsonPocoDeserializerState state
// to simply create a list by Adding(). Not the fastest approach :-(
int elementIndex = 0;
bool? onlyNulls = null;
elementIndex = delayedValidations.GetPropertyIndex(propertyName);
if (elementIndex > 0)
{
state.Path.IncrementIndex(elementIndex);
state.Errors.Add(ERR.DUPLICATE_ARRAY(ref reader, state.Path.GetInstancePath()));
}

while (reader.TokenType != JsonTokenType.EndArray)
{
Expand All @@ -528,6 +548,8 @@ FhirJsonPocoDeserializerState state
existingList[elementIndex] ??= propertyValueMapping.Factory();
onlyNulls = false;
_ = DeserializeFhirPrimitive((PrimitiveType)existingList[elementIndex]!, propertyName, propertyValueMapping, fhirType, ref reader, delayedValidations, state);

delayedValidations.SetPropertyIndex(propertyName, existingList.Count);
}

elementIndex += 1;
Expand Down Expand Up @@ -560,7 +582,7 @@ internal PrimitiveType DeserializeFhirPrimitive(
ClassMapping propertyValueMapping,
Type? fhirType,
ref Utf8JsonReader reader,
DelayedValidations? delayedValidations,
ObjectParsingState? delayedValidations,
FhirJsonPocoDeserializerState state
)
{
Expand Down Expand Up @@ -618,9 +640,10 @@ FhirJsonPocoDeserializerState state
if (delayedValidations is null)
PocoDeserializationHelper.RunInstanceValidation(targetPrimitive, Settings.Validator, context, state.Errors);
else
delayedValidations.Schedule(
delayedValidations.ScheduleDelayedValidation(
propertyName.TrimStart('_') + INSTANCE_VALIDATION_KEY_SUFFIX,
() => {
() =>
{
context.PathStack.EnterElement(propertyName.TrimStart('_'), null, propertyValueMapping.IsPrimitive);
PocoDeserializationHelper.RunInstanceValidation(targetPrimitive, Settings.Validator, context, state.Errors);
context.PathStack.ExitElement();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,47 @@ public void JsonInvalidDuplicateArray()
}
}

[TestMethod]
public void JsonInvalidDuplicateArrayExtension()
{
// string containing a FHIR Patient with name John Doe, 17 Jan 1970, an invalid gender and an invalid date of birth
string rawData = """
{
"resourceType": "Patient",
"id": "pat1",
"name": [
{
"_given": [ null, { "id": "e1" }],
"given": [ "Jane", "John" ],
"given": [ "Rita", true ],
"_given": [ null, { "id": "e2" }]
}
]
}
""";
// This feels like a breaking case and should be fatal if there are more than 1 name/_name

try
{
var p = serializeResource<Patient>(rawData);
DebugDump.OutputJson(p);
Assert.Fail("Expected to throw parsing");
}
catch (DeserializationFailedException ex)
{
System.Diagnostics.Trace.WriteLine($"{ex.Message}");
OperationOutcome oc = ex.ToOperationOutcome();
DebugDump.OutputXml(oc);
DebugDump.OutputJson(ex.PartialResult);

Assert.AreEqual("Patient.name[0].given[2]", oc.Issue[0].Expression.First());
Assert.AreEqual(OperationOutcome.IssueSeverity.Warning, oc.Issue[0].Severity);
Assert.AreEqual("JSON128", oc.Issue[0].Details.Coding[0].Code);

Assert.AreEqual(3, oc.Issue.Count);
}
}

[TestMethod]
public void JsonInvalidElementIdArrayPath()
{
Expand Down

0 comments on commit f8803ad

Please sign in to comment.