Skip to content

Commit 74c06a9

Browse files
authored
Expose ReferenceResolver and rename ReferenceHandling to ReferenceHandler (#36829) (#37296)
* Expose ReferenceResolver and rename ReferenceHandling to ReferenceHandler * Address some feedback * Address feedback * Clean-up code * Change messages in string.resx * Add test for a badly implemented resolver * Address feedback.
1 parent d224df1 commit 74c06a9

27 files changed

+496
-259
lines changed

src/libraries/System.Text.Json/ref/System.Text.Json.cs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ public JsonSerializerOptions(System.Text.Json.JsonSerializerDefaults defaults) {
222222
public bool PropertyNameCaseInsensitive { get { throw null; } set { } }
223223
public System.Text.Json.JsonNamingPolicy? PropertyNamingPolicy { get { throw null; } set { } }
224224
public System.Text.Json.JsonCommentHandling ReadCommentHandling { get { throw null; } set { } }
225-
public System.Text.Json.Serialization.ReferenceHandling ReferenceHandling { get { throw null; } set { } }
225+
public System.Text.Json.Serialization.ReferenceHandler? ReferenceHandler { get { throw null; } set { } }
226226
public bool WriteIndented { get { throw null; } set { } }
227227
public System.Text.Json.Serialization.JsonConverter GetConverter(System.Type typeToConvert) { throw null; }
228228
}
@@ -535,10 +535,22 @@ public JsonStringEnumConverter(System.Text.Json.JsonNamingPolicy? namingPolicy =
535535
public override bool CanConvert(System.Type typeToConvert) { throw null; }
536536
public override System.Text.Json.Serialization.JsonConverter CreateConverter(System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options) { throw null; }
537537
}
538-
public sealed partial class ReferenceHandling
538+
public abstract partial class ReferenceHandler
539539
{
540-
internal ReferenceHandling() { }
541-
public static System.Text.Json.Serialization.ReferenceHandling Default { get { throw null; } }
542-
public static System.Text.Json.Serialization.ReferenceHandling Preserve { get { throw null; } }
540+
protected ReferenceHandler() { }
541+
public static System.Text.Json.Serialization.ReferenceHandler Preserve { get { throw null; } }
542+
public abstract System.Text.Json.Serialization.ReferenceResolver CreateResolver();
543+
}
544+
public sealed partial class ReferenceHandler<T> : System.Text.Json.Serialization.ReferenceHandler where T : System.Text.Json.Serialization.ReferenceResolver, new()
545+
{
546+
public ReferenceHandler() { }
547+
public override System.Text.Json.Serialization.ReferenceResolver CreateResolver() { throw null; }
548+
}
549+
public abstract partial class ReferenceResolver
550+
{
551+
protected ReferenceResolver() { }
552+
public abstract void AddReference(string referenceId, object value);
553+
public abstract string GetReference(object value, out bool alreadyExists);
554+
public abstract object ResolveReference(string referenceId);
543555
}
544556
}

src/libraries/System.Text.Json/src/Resources/Strings.resx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@
418418
<value>Either the JSON value is not in a supported format, or is out of bounds for a UInt16.</value>
419419
</data>
420420
<data name="SerializerCycleDetected" xml:space="preserve">
421-
<value>A possible object cycle was detected. This can either be due to a cycle or if the object depth is larger than the maximum allowed depth of {0}. Consider using ReferenceHandling.Preserve on JsonSerializerOptions to support cycles.</value>
421+
<value>A possible object cycle was detected. This can either be due to a cycle or if the object depth is larger than the maximum allowed depth of {0}. Consider using ReferenceHandler.Preserve on JsonSerializerOptions to support cycles.</value>
422422
</data>
423423
<data name="EmptyStringToInitializeNumber" xml:space="preserve">
424424
<value>Expected a number, but instead got empty string.</value>
@@ -480,7 +480,7 @@
480480
<value>The '$id' and '$ref' metadata properties must be JSON strings. Current token type is '{0}'.</value>
481481
</data>
482482
<data name="MetadataInvalidPropertyWithLeadingDollarSign" xml:space="preserve">
483-
<value>Properties that start with '$' are not allowed on preserve mode, either escape the character or turn off preserve references by setting ReferenceHandling to ReferenceHandling.Default.</value>
483+
<value>Properties that start with '$' are not allowed on preserve mode, either escape the character or turn off preserve references by setting ReferenceHandler to null.</value>
484484
</data>
485485
<data name="MultipleMembersBindWithConstructorParameter" xml:space="preserve">
486486
<value>Members '{0}' and '{1}' on type '{2}' cannot both bind with parameter '{3}' in constructor '{4}' on deserialization.</value>
@@ -527,4 +527,4 @@
527527
<data name="DefaultIgnoreConditionInvalid" xml:space="preserve">
528528
<value>The value cannot be 'JsonIgnoreCondition.Always'.</value>
529529
</data>
530-
</root>
530+
</root>

src/libraries/System.Text.Json/src/System.Text.Json.csproj

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,6 @@
123123
<Compile Include="System\Text\Json\Serialization\Converters\Value\UInt32Converter.cs" />
124124
<Compile Include="System\Text\Json\Serialization\Converters\Value\UInt64Converter.cs" />
125125
<Compile Include="System\Text\Json\Serialization\Converters\Value\UriConverter.cs" />
126-
<Compile Include="System\Text\Json\Serialization\DefaultReferenceResolver.cs" />
127126
<Compile Include="System\Text\Json\Serialization\JsonCamelCaseNamingPolicy.cs" />
128127
<Compile Include="System\Text\Json\Serialization\JsonClassInfo.cs" />
129128
<Compile Include="System\Text\Json\Serialization\JsonClassInfo.Cache.cs" />
@@ -162,10 +161,14 @@
162161
<Compile Include="System\Text\Json\Serialization\MetadataPropertyName.cs" />
163162
<Compile Include="System\Text\Json\Serialization\ParameterRef.cs" />
164163
<Compile Include="System\Text\Json\Serialization\PooledByteBufferWriter.cs" />
164+
<Compile Include="System\Text\Json\Serialization\PreserveReferenceHandler.cs" />
165+
<Compile Include="System\Text\Json\Serialization\PreserveReferenceResolver.cs" />
165166
<Compile Include="System\Text\Json\Serialization\PropertyRef.cs" />
166167
<Compile Include="System\Text\Json\Serialization\ReadStack.cs" />
167168
<Compile Include="System\Text\Json\Serialization\ReadStackFrame.cs" />
168-
<Compile Include="System\Text\Json\Serialization\ReferenceHandling.cs" />
169+
<Compile Include="System\Text\Json\Serialization\ReferenceHandler.cs" />
170+
<Compile Include="System\Text\Json\Serialization\ReferenceHandlerOfT.cs" />
171+
<Compile Include="System\Text\Json\Serialization\ReferenceResolver.cs" />
169172
<Compile Include="System\Text\Json\Serialization\ReflectionEmitMemberAccessor.cs" />
170173
<Compile Include="System\Text\Json\Serialization\ReflectionMemberAccessor.cs" />
171174
<Compile Include="System\Text\Json\Serialization\StackFrameObjectState.cs" />

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,7 @@ internal sealed override bool OnTryRead(
6969
ref ReadStack state,
7070
[MaybeNullWhen(false)] out TCollection value)
7171
{
72-
bool shouldReadPreservedReferences = options.ReferenceHandling.ShouldReadPreservedReferences();
73-
74-
if (!state.SupportContinuation && !shouldReadPreservedReferences)
72+
if (state.UseFastPath)
7573
{
7674
// Fast path that avoids maintaining state variables and dealing with preserved references.
7775

@@ -148,7 +146,8 @@ internal sealed override bool OnTryRead(
148146
}
149147

150148
// Handle the metadata properties.
151-
if (shouldReadPreservedReferences && state.Current.ObjectState < StackFrameObjectState.PropertyValue)
149+
bool preserveReferences = options.ReferenceHandler != null;
150+
if (preserveReferences && state.Current.ObjectState < StackFrameObjectState.PropertyValue)
152151
{
153152
if (JsonSerializer.ResolveMetadata(this, ref reader, ref state))
154153
{
@@ -175,10 +174,10 @@ internal sealed override bool OnTryRead(
175174
Debug.Assert(CanHaveIdMetadata);
176175

177176
value = (TCollection)state.Current.ReturnValue!;
178-
if (!state.ReferenceResolver.AddReferenceOnDeserialize(state.Current.MetadataId, value))
179-
{
180-
ThrowHelper.ThrowJsonException_MetadataDuplicateIdFound(state.Current.MetadataId, ref state);
181-
}
177+
state.ReferenceResolver.AddReference(state.Current.MetadataId, value);
178+
// Clear metadata name, if the next read fails
179+
// we want to point the JSON path to the property's object.
180+
state.Current.JsonPropertyName = null;
182181
}
183182

184183
state.Current.ObjectState = StackFrameObjectState.CreatedObject;
@@ -214,7 +213,7 @@ internal sealed override bool OnTryRead(
214213
state.Current.PropertyState = StackFramePropertyState.Name;
215214

216215
// Verify property doesn't contain metadata.
217-
if (shouldReadPreservedReferences)
216+
if (preserveReferences)
218217
{
219218
ReadOnlySpan<byte> propertyName = reader.GetSpan();
220219
if (propertyName.Length > 0 && propertyName[0] == '$')
@@ -275,7 +274,7 @@ internal sealed override bool OnTryWrite(
275274
state.Current.ProcessedStartToken = true;
276275
writer.WriteStartObject();
277276

278-
if (options.ReferenceHandling.ShouldWritePreservedReferences())
277+
if (options.ReferenceHandler != null)
279278
{
280279
if (JsonSerializer.WriteReferenceForObject(this, dictionary, ref state, writer) == MetadataPropertyName.Ref)
281280
{

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableDefaultConverter.cs

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,7 @@ internal override bool OnTryRead(
4040
ref ReadStack state,
4141
[MaybeNullWhen(false)] out TCollection value)
4242
{
43-
bool shouldReadPreservedReferences = options.ReferenceHandling.ShouldReadPreservedReferences();
44-
45-
if (!state.SupportContinuation && !shouldReadPreservedReferences)
43+
if (state.UseFastPath)
4644
{
4745
// Fast path that avoids maintaining state variables and dealing with preserved references.
4846

@@ -91,13 +89,14 @@ internal override bool OnTryRead(
9189
{
9290
// Slower path that supports continuation and preserved references.
9391

92+
bool preserveReferences = options.ReferenceHandler != null;
9493
if (state.Current.ObjectState == StackFrameObjectState.None)
9594
{
9695
if (reader.TokenType == JsonTokenType.StartArray)
9796
{
9897
state.Current.ObjectState = StackFrameObjectState.PropertyValue;
9998
}
100-
else if (shouldReadPreservedReferences)
99+
else if (preserveReferences)
101100
{
102101
if (reader.TokenType != JsonTokenType.StartObject)
103102
{
@@ -113,7 +112,7 @@ internal override bool OnTryRead(
113112
}
114113

115114
// Handle the metadata properties.
116-
if (shouldReadPreservedReferences && state.Current.ObjectState < StackFrameObjectState.PropertyValue)
115+
if (preserveReferences && state.Current.ObjectState < StackFrameObjectState.PropertyValue)
117116
{
118117
if (JsonSerializer.ResolveMetadata(this, ref reader, ref state))
119118
{
@@ -137,10 +136,17 @@ internal override bool OnTryRead(
137136
if (state.Current.MetadataId != null)
138137
{
139138
value = (TCollection)state.Current.ReturnValue!;
140-
if (!state.ReferenceResolver.AddReferenceOnDeserialize(state.Current.MetadataId, value))
141-
{
142-
ThrowHelper.ThrowJsonException_MetadataDuplicateIdFound(state.Current.MetadataId, ref state);
143-
}
139+
140+
// TODO: https://github.com/dotnet/runtime/issues/37168
141+
//Separate logic for IEnumerable to call AddReference when the reader is at `$id`, in order to avoid remembering the last metadata.
142+
143+
// Remember the prior metadata and temporarily use '$id' to write it in the path in case AddReference throws
144+
// in this case, the last property seen will be '$values' when we reach this point.
145+
byte[]? lastMetadataProperty = state.Current.JsonPropertyName;
146+
state.Current.JsonPropertyName = JsonSerializer.s_idPropertyName;
147+
148+
state.ReferenceResolver.AddReference(state.Current.MetadataId, value);
149+
state.Current.JsonPropertyName = lastMetadataProperty;
144150
}
145151

146152
state.Current.JsonPropertyInfo = state.Current.JsonClassInfo.ElementClassInfo!.PropertyInfoForClassInfo;
@@ -247,13 +253,11 @@ internal sealed override bool OnTryWrite(Utf8JsonWriter writer, TCollection valu
247253
}
248254
else
249255
{
250-
bool shouldWritePreservedReferences = options.ReferenceHandling.ShouldWritePreservedReferences();
251-
252256
if (!state.Current.ProcessedStartToken)
253257
{
254258
state.Current.ProcessedStartToken = true;
255259

256-
if (!shouldWritePreservedReferences)
260+
if (options.ReferenceHandler == null)
257261
{
258262
writer.WriteStartArray();
259263
}

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,9 @@ internal class ObjectDefaultConverter<T> : JsonObjectConverter<T> where T : notn
1414
{
1515
internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, [MaybeNullWhen(false)] out T value)
1616
{
17-
bool shouldReadPreservedReferences = options.ReferenceHandling.ShouldReadPreservedReferences();
1817
object obj;
1918

20-
if (!state.SupportContinuation && !shouldReadPreservedReferences)
19+
if (state.UseFastPath)
2120
{
2221
// Fast path that avoids maintaining state variables and dealing with preserved references.
2322

@@ -76,7 +75,7 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert,
7675
// Handle the metadata properties.
7776
if (state.Current.ObjectState < StackFrameObjectState.PropertyValue)
7877
{
79-
if (shouldReadPreservedReferences)
78+
if (options.ReferenceHandler != null)
8079
{
8180
if (JsonSerializer.ResolveMetadata(this, ref reader, ref state))
8281
{
@@ -106,10 +105,10 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert,
106105
obj = state.Current.JsonClassInfo.CreateObject!()!;
107106
if (state.Current.MetadataId != null)
108107
{
109-
if (!state.ReferenceResolver.AddReferenceOnDeserialize(state.Current.MetadataId, obj))
110-
{
111-
ThrowHelper.ThrowJsonException_MetadataDuplicateIdFound(state.Current.MetadataId, ref state);
112-
}
108+
state.ReferenceResolver.AddReference(state.Current.MetadataId, obj);
109+
// Clear metadata name, if the next read fails
110+
// we want to point the JSON path to the property's object.
111+
state.Current.JsonPropertyName = null;
113112
}
114113

115114
state.Current.ReturnValue = obj;
@@ -239,7 +238,7 @@ internal sealed override bool OnTryWrite(Utf8JsonWriter writer, T value, JsonSer
239238
{
240239
writer.WriteStartObject();
241240

242-
if (options.ReferenceHandling.ShouldWritePreservedReferences())
241+
if (options.ReferenceHandler != null)
243242
{
244243
if (JsonSerializer.WriteReferenceForObject(this, objectValue, ref state, writer) == MetadataPropertyName.Ref)
245244
{
@@ -294,7 +293,7 @@ internal sealed override bool OnTryWrite(Utf8JsonWriter writer, T value, JsonSer
294293
{
295294
writer.WriteStartObject();
296295

297-
if (options.ReferenceHandling.ShouldWritePreservedReferences())
296+
if (options.ReferenceHandler != null)
298297
{
299298
if (JsonSerializer.WriteReferenceForObject(this, objectValue, ref state, writer) == MetadataPropertyName.Ref)
300299
{

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,9 @@ internal abstract partial class ObjectWithParameterizedConstructorConverter<T> :
2121
{
2222
internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, [MaybeNullWhen(false)] out T value)
2323
{
24-
bool shouldReadPreservedReferences = options.ReferenceHandling.ShouldReadPreservedReferences();
2524
object obj;
2625

27-
if (!state.SupportContinuation && !shouldReadPreservedReferences)
26+
if (state.UseFastPath)
2827
{
2928
// Fast path that avoids maintaining state variables.
3029

0 commit comments

Comments
 (0)