@@ -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