@@ -47,18 +47,25 @@ public void Serialize(Base instance, Utf8JsonWriter writer, Func<SerializationFi
4747 if ( filter is not null )
4848 instance = SerializationUtil . MakeSubsettedClone ( instance ) ;
4949
50+ // Structures that turn out to be empty must not be written at all, but we only know that after we
51+ // have walked their members. The PruningJsonWriter postpones the opening tokens for us, so the
52+ // serializer below can simply write, and never has to take an empty object back.
53+ // Note that the root is written even when it is empty: our public API promises Json output, and
54+ // callers like SerializeToDocument() would choke on an empty payload.
55+ var pruningWriter = new PruningJsonWriter ( writer ) ;
56+
5057 // This handles an edge-case where we are asked to serialize just a primitive value.
5158 // For compatibility with SDK5 logic, we emit object with pseudo-property 'value' and value of the fhir primitive.
5259 // Issue for context: https://github.com/FirelyTeam/firely-net-sdk/issues/3286
5360 if ( instance is not PrimitiveType val )
5461 {
55- serializeInternal ( instance , writer , filter ) ;
62+ serializeInternal ( instance , null , pruningWriter , filter , PruningJsonWriter . OnEmpty . Keep ) ;
5663 }
5764 else
5865 {
59- writer . WriteStartObject ( ) ;
60- serializeFhirPrimitive ( "value" , val , writer , filter ) ;
61- writer . WriteEndObject ( ) ;
66+ pruningWriter . WriteStartObject ( onEmpty : PruningJsonWriter . OnEmpty . Keep ) ;
67+ serializeFhirPrimitive ( "value" , val , pruningWriter , filter ) ;
68+ pruningWriter . WriteEndObject ( ) ;
6269 }
6370 }
6471
@@ -67,38 +74,48 @@ public void Serialize(Base instance, Utf8JsonWriter writer, Func<SerializationFi
6774 /// </summary>
6875 /// <remarks>Not serializing the "value" element is useful when serializing FHIR primitives into two properties, one
6976 /// with just the value, and one with the id/extensions.</remarks>
77+ /// <param name="element">The element to serialize, or <c>null</c> to write a placeholder.</param>
78+ /// <param name="propertyName">The name of the property the element is the value of, or <c>null</c> when it
79+ /// is an item of an array. It is passed to the writer along with the opening brace, so that it can be
80+ /// dropped together with it when the element turns out to be empty.</param>
81+ /// <param name="writer">The writer to serialize into.</param>
82+ /// <param name="filter">An optional filter determining which members to serialize.</param>
83+ /// <param name="onEmpty">What the writer should do when the element produces no output at all.</param>
7084 private void serializeInternal (
7185 Base ? element ,
72- Utf8JsonWriter writer ,
73- SerializationFilter ? filter )
86+ string ? propertyName ,
87+ PruningJsonWriter writer ,
88+ SerializationFilter ? filter ,
89+ PruningJsonWriter . OnEmpty onEmpty = PruningJsonWriter . OnEmpty . Omit )
7490 {
7591 if ( element is null )
7692 {
7793 // empty objects in arrays may occur in error situations.
78- writer . WriteNullValue ( ) ;
94+ writer . Value . WriteNullValue ( ) ;
7995 return ;
8096 }
8197
82- writer . WriteStartObject ( ) ;
83-
84- if ( element is Resource r and not DynamicResource { DynamicTypeName : null } )
85- writer . WriteString ( "resourceType" , r . TypeName ) ;
86-
8798 // Only throw if we don't have a mapping where we are expected to: when this is a subclass of Base.
99+ // Resolved before any output is written, so a failure does not leave the writer in a broken state.
88100 if ( Inspector . FindOrImportClassMapping ( element ) is not { } mapping )
89101 throw new InvalidOperationException ( $ "Encountered type { element . GetType ( ) } , which is a support POCO for FHIR, but does not " +
90102 $ "have sufficient metadata to be used by the serializer.") ;
91103
104+ writer . WriteStartObject ( propertyName , onEmpty ) ;
105+
106+ if ( element is Resource r and not DynamicResource { DynamicTypeName : null } )
107+ writer . WriteString ( "resourceType" , r . TypeName ) ;
108+
92109 filter ? . EnterObject ( element , mapping ) ;
93110
94111 foreach ( var member in element . EnumerateElements ( ) )
95112 {
96- var propertyMapping = mapping ? . FindMappedElementByName ( member . Key ) ;
113+ var propertyMapping = mapping . FindMappedElementByName ( member . Key ) ;
97114
98115 if ( filter ? . TryEnterMember ( member . Key , member . Value , propertyMapping ) == false )
99116 continue ;
100117
101- var propertyName = propertyMapping switch
118+ var memberPropertyName = propertyMapping switch
102119 {
103120 { Choice : ChoiceType . DatatypeChoice } => addSuffixToElementName ( member . Key , member . Value ) ,
104121 null when member . Value is DataType annotatable && annotatable . HasAnnotation < ChoiceElementAnnotation > ( )
@@ -109,26 +126,24 @@ private void serializeInternal(
109126 switch ( member . Value )
110127 {
111128 case PrimitiveType pt :
112- serializeFhirPrimitive ( propertyName , pt , writer , filter ) ;
129+ serializeFhirPrimitive ( memberPropertyName , pt , writer , filter ) ;
113130 break ;
114131 case IReadOnlyList < PrimitiveType ? > pts :
115- serializeFhirPrimitiveList ( propertyName , pts , writer , filter ) ;
132+ serializeFhirPrimitiveList ( memberPropertyName , pts , writer , filter ) ;
116133 break ;
117134 case IReadOnlyList < Base ? > children : // Not List<Base>, since that is an invariant type.
118135 {
119- writer . WritePropertyName ( propertyName ) ;
120- writer . WriteStartArray ( ) ;
136+ writer . WriteStartArray ( memberPropertyName ) ;
121137
122138 foreach ( var child in children )
123- serializeInternal ( child , writer , filter ) ;
139+ serializeInternal ( child , null , writer , filter ) ;
124140
125141 writer . WriteEndArray ( ) ;
126142 break ;
127143 }
128144 case Base b :
129145 {
130- writer . WritePropertyName ( propertyName ) ;
131- serializeInternal ( b , writer , filter ) ;
146+ serializeInternal ( b , memberPropertyName , writer , filter ) ;
132147 break ;
133148 }
134149 default :
@@ -163,81 +178,44 @@ private static string addSuffixToElementName(string elementName, object elementV
163178 private void serializeFhirPrimitiveList (
164179 string elementName ,
165180 IReadOnlyList < PrimitiveType ? > values ,
166- Utf8JsonWriter writer ,
181+ PruningJsonWriter writer ,
167182 SerializationFilter ? filter )
168183 {
169- if ( values is null ) throw new ArgumentNullException ( nameof ( values ) ) ;
184+ if ( values is null ) throw new ArgumentNullException ( nameof ( values ) ) ;
170185
171186 // Don't serialize empty collections.
172187 if ( values . Count == 0 ) return ;
173188
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 ;
189+ // The "elementName" and "_elementName" arrays must have the same length, so that each id/extension
190+ // lines up with the value it belongs to. Entries without content therefore become placeholders
191+ // rather than being left out. Neither array should be written at all, however, if none of the
192+ // entries has content - which is what the writer's placeholders take care of: it only writes them
193+ // once the array turns out to hold something.
194+ writer . WriteStartArray ( elementName ) ;
180195
181196 foreach ( var value in values )
182197 {
183198 if ( value ? . JsonValue is not null )
184- {
185- if ( ! wroteStartArray )
186- {
187- wroteStartArray = true ;
188- writeStartArray ( elementName , numNullsMissed , writer ) ;
189- }
190-
191- SerializePrimitiveValue ( value , writer ) ;
192- }
199+ SerializePrimitiveValue ( value , writer . Value ) ;
193200 else
194- {
195- if ( wroteStartArray )
196- writer . WriteNullValue ( ) ;
197- else
198- numNullsMissed += 1 ;
199- }
201+ writer . WriteNullPlaceholder ( ) ;
200202 }
201203
202- if ( wroteStartArray ) writer . WriteEndArray ( ) ;
204+ writer . WriteEndArray ( ) ;
203205
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 ;
206+ writer . WriteStartArray ( "_" + elementName ) ;
210207
211208 foreach ( var value in values )
212209 {
213- if ( value ? . EnumerateElements ( ) . Any ( ) == true )
214- {
215- if ( ! wroteStartArray )
216- {
217- wroteStartArray = true ;
218- writeStartArray ( "_" + elementName , numNullsMissed , writer ) ;
219- }
220-
221- serializeInternal ( value , writer , filter ) ;
222- }
210+ // As in serializeFhirPrimitive: a value without id/extensions can only ever become a
211+ // placeholder, so there is no point descending into it.
212+ if ( value ? . EnumerateElements ( ) . Any ( ) != true )
213+ writer . WriteNullPlaceholder ( ) ;
223214 else
224- {
225- if ( wroteStartArray )
226- writer . WriteNullValue ( ) ;
227- else
228- numNullsMissed += 1 ;
229- }
215+ serializeInternal ( value , null , writer , filter , PruningJsonWriter . OnEmpty . NullPlaceholder ) ;
230216 }
231217
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 ( ) ;
218+ writer . WriteEndArray ( ) ;
241219 }
242220
243221
@@ -246,22 +224,27 @@ private static void writeStartArray(string propName, int numNulls, Utf8JsonWrite
246224 /// </summary>
247225 /// <remarks>FHIR primitives are handled separately here since they may require
248226 /// serialization into two Json properties called "elementName" and "_elementName".</remarks>
249- private void serializeFhirPrimitive ( string elementName , PrimitiveType value , Utf8JsonWriter writer , SerializationFilter ? filter )
227+ private void serializeFhirPrimitive ( string elementName , PrimitiveType value , PruningJsonWriter writer , SerializationFilter ? filter )
250228 {
251229 if ( value is null ) throw new ArgumentNullException ( nameof ( value ) ) ;
252230
253231 if ( value . JsonValue is not null )
254232 {
255233 // Write a property with 'elementName'
256234 writer . WritePropertyName ( elementName ) ;
257- SerializePrimitiveValue ( value , writer ) ;
235+ SerializePrimitiveValue ( value , writer . Value ) ;
258236 }
259237
238+ // A primitive without id/extensions has nothing to put in '_elementName', so don't descend into it.
239+ // This is just a shortcut for the most common case - the writer would drop the resulting empty
240+ // object anyway - but it also keeps the filter callbacks for such a primitive as they were.
260241 if ( ! value . EnumerateElements ( ) . Any ( ) ) return ;
261-
262- deferSerializeForFilter ( elementName , value , writer , filter ) ;
242+
243+ // Write a property with '_elementName' for the id/extensions - which the writer leaves out
244+ // altogether when the filter turns out to have removed all of them.
245+ serializeInternal ( value , "_" + elementName , writer , filter ) ;
263246 }
264-
247+
265248 private static void tryWriteBase64 ( Utf8JsonWriter writer , string text )
266249 {
267250 var maxSize = Base64 . GetMaxDecodedFromUtf8Length ( text . Length ) ;
@@ -272,24 +255,6 @@ private static void tryWriteBase64(Utf8JsonWriter writer, string text)
272255 writer . WriteStringValue ( text ) ;
273256 }
274257
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-
293258 /// <summary>
294259 /// Serialize a primitive POCO into Json.
295260 /// </summary>
0 commit comments