Skip to content

Commit 1cf75cc

Browse files
committed
Omit empty JSON containers without buffering
1 parent 3ccb4d3 commit 1cf75cc

5 files changed

Lines changed: 402 additions & 171 deletions

src/Hl7.Fhir.Base/Serialization/BaseFhirJsonSerializer.cs

Lines changed: 40 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -47,18 +47,19 @@ public void Serialize(Base instance, Utf8JsonWriter writer, Func<SerializationFi
4747
if (filter is not null)
4848
instance = SerializationUtil.MakeSubsettedClone(instance);
4949

50+
var deferredWriter = new DeferredJsonWriter(writer);
51+
5052
// This handles an edge-case where we are asked to serialize just a primitive value.
5153
// For compatibility with SDK5 logic, we emit object with pseudo-property 'value' and value of the fhir primitive.
5254
// Issue for context: https://github.com/FirelyTeam/firely-net-sdk/issues/3286
5355
if (instance is not PrimitiveType val)
5456
{
55-
serializeInternal(instance, writer, filter);
57+
serializeInternal(instance, deferredWriter, filter, required: true);
5658
}
5759
else
5860
{
59-
writer.WriteStartObject();
60-
serializeFhirPrimitive("value", val, writer, filter);
61-
writer.WriteEndObject();
61+
using var root = deferredWriter.BeginObject(required: true);
62+
serializeFhirPrimitive("value", val, deferredWriter, filter);
6263
}
6364
}
6465

@@ -69,8 +70,11 @@ public void Serialize(Base instance, Utf8JsonWriter writer, Func<SerializationFi
6970
/// with just the value, and one with the id/extensions.</remarks>
7071
private void serializeInternal(
7172
Base? element,
72-
Utf8JsonWriter writer,
73-
SerializationFilter? filter)
73+
DeferredJsonWriter writer,
74+
SerializationFilter? filter,
75+
string? containerPropertyName = null,
76+
bool required = false,
77+
bool writeNullIfEmpty = false)
7478
{
7579
if (element is null)
7680
{
@@ -79,16 +83,16 @@ private void serializeInternal(
7983
return;
8084
}
8185

82-
writer.WriteStartObject();
83-
84-
if (element is Resource r and not DynamicResource { DynamicTypeName: null })
85-
writer.WriteString("resourceType", r.TypeName);
86-
8786
// Only throw if we don't have a mapping where we are expected to: when this is a subclass of Base.
8887
if (Inspector.FindOrImportClassMapping(element) is not {} mapping)
8988
throw new InvalidOperationException($"Encountered type {element.GetType()}, which is a support POCO for FHIR, but does not " +
9089
$"have sufficient metadata to be used by the serializer.");
9190

91+
using var objectScope = writer.BeginObject(containerPropertyName, required, writeNullIfEmpty);
92+
93+
if (element is Resource r and not DynamicResource { DynamicTypeName: null })
94+
writer.WriteString("resourceType", r.TypeName);
95+
9296
filter?.EnterObject(element, mapping);
9397

9498
foreach (var member in element.EnumerateElements())
@@ -116,19 +120,15 @@ private void serializeInternal(
116120
break;
117121
case IReadOnlyList<Base?> children: // Not List<Base>, since that is an invariant type.
118122
{
119-
writer.WritePropertyName(propertyName);
120-
writer.WriteStartArray();
123+
using var arrayScope = writer.BeginArray(propertyName);
121124

122125
foreach (var child in children)
123126
serializeInternal(child, writer, filter);
124-
125-
writer.WriteEndArray();
126127
break;
127128
}
128129
case Base b:
129130
{
130-
writer.WritePropertyName(propertyName);
131-
serializeInternal(b, writer, filter);
131+
serializeInternal(b, writer, filter, propertyName);
132132
break;
133133
}
134134
default:
@@ -139,7 +139,6 @@ private void serializeInternal(
139139
}
140140

141141
filter?.LeaveObject(element, mapping);
142-
writer.WriteEndObject();
143142
}
144143

145144
private static string addSuffixToElementName(string elementName, object elementValue)
@@ -163,81 +162,41 @@ private static string addSuffixToElementName(string elementName, object elementV
163162
private void serializeFhirPrimitiveList(
164163
string elementName,
165164
IReadOnlyList<PrimitiveType?> values,
166-
Utf8JsonWriter writer,
165+
DeferredJsonWriter writer,
167166
SerializationFilter? filter)
168167
{
169-
if(values is null) throw new ArgumentNullException(nameof(values));
168+
if (values is null) throw new ArgumentNullException(nameof(values));
170169

171170
// Don't serialize empty collections.
172171
if (values.Count == 0) return;
173172

174-
// We should not write a "elementName" property until we encounter an actual
175-
// value. If we do, we should "catch up", by creating the property starting
176-
// with a json array that contains 'null' for each of the elements we encountered
177-
// until now that did not have a value id/extensions.
178-
bool wroteStartArray = false;
179-
int numNullsMissed = 0;
180-
181-
foreach (var value in values)
173+
// Nulls that precede the first actual value are deferred. This lets us omit the property
174+
// when every item lacks a value while preserving positional alignment once one is found.
175+
using (writer.BeginArray(elementName))
182176
{
183-
if (value?.JsonValue is not null)
184-
{
185-
if (!wroteStartArray)
177+
foreach (var value in values)
178+
if (value?.JsonValue is not null)
186179
{
187-
wroteStartArray = true;
188-
writeStartArray(elementName, numNullsMissed, writer);
180+
SerializePrimitiveValue(value, writer.PrepareArrayValue());
189181
}
190-
191-
SerializePrimitiveValue(value, writer);
192-
}
193-
else
194-
{
195-
if (wroteStartArray)
196-
writer.WriteNullValue();
197182
else
198-
numNullsMissed += 1;
199-
}
200-
}
201-
202-
if (wroteStartArray) writer.WriteEndArray();
203-
204-
// We should not write a "_elementName" property until we encounter an actual
205-
// id/extension. If we do, we should "catch up", by creating the property starting
206-
// with a json array that contains 'null' for each of the elements we encountered
207-
// until now that did not have id/extensions etc.
208-
wroteStartArray = false;
209-
numNullsMissed = 0;
210-
211-
foreach (var value in values)
212-
{
213-
if (value?.EnumerateElements().Any() == true)
214-
{
215-
if (!wroteStartArray)
216183
{
217-
wroteStartArray = true;
218-
writeStartArray("_" + elementName, numNullsMissed, writer);
184+
writer.DeferNullValue();
219185
}
186+
}
220187

221-
serializeInternal(value, writer, filter);
222-
}
223-
else
188+
// Empty metadata objects become deferred null placeholders. If a later object survives
189+
// filtering, the placeholders are emitted before it; if none survive, the array is omitted.
190+
using (writer.BeginArray("_" + elementName))
191+
{
192+
foreach (var value in values)
224193
{
225-
if (wroteStartArray)
226-
writer.WriteNullValue();
194+
if (value is not null && value.EnumerateElements().Any())
195+
serializeInternal(value, writer, filter, writeNullIfEmpty: true);
227196
else
228-
numNullsMissed += 1;
197+
writer.DeferNullValue();
229198
}
230199
}
231-
232-
if (wroteStartArray) writer.WriteEndArray();
233-
}
234-
235-
private static void writeStartArray(string propName, int numNulls, Utf8JsonWriter writer)
236-
{
237-
writer.WriteStartArray(propName);
238-
239-
for (int i = 0; i < numNulls; i++)
240-
writer.WriteNullValue();
241200
}
242201

243202

@@ -246,20 +205,18 @@ private static void writeStartArray(string propName, int numNulls, Utf8JsonWrite
246205
/// </summary>
247206
/// <remarks>FHIR primitives are handled separately here since they may require
248207
/// serialization into two Json properties called "elementName" and "_elementName".</remarks>
249-
private void serializeFhirPrimitive(string elementName, PrimitiveType value, Utf8JsonWriter writer, SerializationFilter? filter)
208+
private void serializeFhirPrimitive(string elementName, PrimitiveType value, DeferredJsonWriter writer, SerializationFilter? filter)
250209
{
251210
if (value is null) throw new ArgumentNullException(nameof(value));
252211

253212
if (value.JsonValue is not null)
254213
{
255-
// Write a property with 'elementName'
256-
writer.WritePropertyName(elementName);
257-
SerializePrimitiveValue(value, writer);
214+
SerializePrimitiveValue(value, writer.PreparePropertyValue(elementName));
258215
}
259216

260217
if (!value.EnumerateElements().Any()) return;
261-
262-
deferSerializeForFilter(elementName, value, writer, filter);
218+
219+
serializeInternal(value, writer, filter, "_" + elementName);
263220
}
264221

265222
private static void tryWriteBase64(Utf8JsonWriter writer, string text)
@@ -272,24 +229,6 @@ private static void tryWriteBase64(Utf8JsonWriter writer, string text)
272229
writer.WriteStringValue(text);
273230
}
274231

275-
private void deferSerializeForFilter(string elementName, PrimitiveType value, Utf8JsonWriter writer, SerializationFilter? filter)
276-
{
277-
var buffer = new ArrayBufferWriter<byte>();
278-
using (var defer = new Utf8JsonWriter(buffer, writer.Options))
279-
{
280-
serializeInternal(value, defer, filter);
281-
}
282-
283-
// brackets only, so either object was empty, or we filtered everything out
284-
const int expectedLength = 3;
285-
if (buffer.WrittenCount < expectedLength) return;
286-
287-
// Write a property with '_elementName'
288-
writer.WritePropertyName("_" + elementName);
289-
// write the deferred data
290-
writer.WriteRawValue(buffer.WrittenSpan, skipInputValidation: true);
291-
}
292-
293232
/// <summary>
294233
/// Serialize a primitive POCO into Json.
295234
/// </summary>
@@ -359,4 +298,4 @@ protected virtual void SerializePrimitiveValue(object? value, Utf8JsonWriter wri
359298
}
360299

361300
[Obsolete("This class has been replaced by the equivalent BaseFhirJsonSerializer class.")]
362-
public class BaseFhirJsonPocoSerializer(ModelInspector inspector) : BaseFhirJsonSerializer(inspector);
301+
public class BaseFhirJsonPocoSerializer(ModelInspector inspector) : BaseFhirJsonSerializer(inspector);

0 commit comments

Comments
 (0)