-
Notifications
You must be signed in to change notification settings - Fork 5.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ReferenceHandler.Preserve does not work well with JsonConverters #21777
Comments
cc @jozkee to confirm the story with Since reference data is only cached per call to On a side note, I'd expect normally a |
You can root the ReferenceResolver instance in the call site of JsonSerializer.Serialize in order to make it persistent on nested calls to Serialize. However, be careful to not expand this pattern to a "global" JsonSerializerOptions, otherwise the reference data will never reset and the underlying dictionaries may grow forever. class Program
{
static void Main(string[] args)
{
var data = new ActionResultDto<object>
{
Result = new Data()
};
var options = new JsonSerializerOptions()
{
ReferenceHandler = ReferenceHandler.Preserve
};
options.Converters.Add(new EmptyJsonConverter());
options.ReferenceHandler = new MyReferenceHandler();
string str = JsonSerializer.Serialize(data, options);
// Always reset or null-out options.ReferenceHandler after you are done serializing
// in order to avoid out of bounds memory growth in the reference resolver.
options.ReferenceHandler = null;
System.Console.WriteLine(str); // Output => {"$id":"1","Result":{"$id":"2"}}
}
}
class MyReferenceHandler : ReferenceHandler
{
public MyReferenceHandler()
{
_rootedResolver = new MyReferenceResolver();
}
private ReferenceResolver _rootedResolver;
public override ReferenceResolver CreateResolver() => _rootedResolver;
class MyReferenceResolver : ReferenceResolver
{
private uint _referenceCount;
private readonly Dictionary<string, object> _referenceIdToObjectMap = new Dictionary<string, object>();
private readonly Dictionary<object, string> _objectToReferenceIdMap = new Dictionary<object, string>(ReferenceEqualityComparer.Instance);
public override void AddReference(string referenceId, object value)
{
if (!_referenceIdToObjectMap.TryAdd(referenceId, value))
{
throw new JsonException();
}
}
public override string GetReference(object value, out bool alreadyExists)
{
if (_objectToReferenceIdMap.TryGetValue(value, out string referenceId))
{
alreadyExists = true;
}
else
{
_referenceCount++;
referenceId = _referenceCount.ToString();
_objectToReferenceIdMap.Add(value, referenceId);
alreadyExists = false;
}
return referenceId;
}
public override object ResolveReference(string referenceId)
{
if (!_referenceIdToObjectMap.TryGetValue(referenceId, out object value))
{
throw new JsonException();
}
return value;
}
}
} |
Transferring this to dotnet/docs to consider adding a sample for
cc @tdykstra |
@tdykstra FYI I added doc-enhancement as the categorization label. Please correct it if I'm mistaken. |
Thanks, that's the right label. |
@layomia or @jozkee can you provide an example suitable for docs for each of these scenarios? Something like the one for non-custom-converter preserve references would be more helpful than a converter for I don't see how to adapt this approach to types other than Also, when I run the example code it fails at |
Assuming that you wrote a custom converter for
Assuming that you have a list of
@tdykstra I fixed that in above examples by adding a
I think that's only problematic when you funnel the call to JsonSerializer.Serialize and T is the same type as your |
@jozkee @tdykstra - as a sidebar to the main discussion of this issue; given the provided example: internal class EmptyJsonConverter : JsonConverter<object>
{
public override object Read(ref Utf8JsonReader reader,Type typeToConvert,JsonSerializerOptions options)
{
throw new InvalidOperationException("Should not get here.");
}
public override void Write(Utf8JsonWriter writer,object objectToWrite,JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, objectToWrite, options);
}
} It just occurred to me that the serializer calls |
@jozkee , I have the same problem but now with IgnoreCycles: I need to add $type property for an array that will contain objects of different types ex : ( [A,B,C] of IEnumerable< A> => [A,{$type:"B",$value:B},{$type:"C",$value:C}] ) private abstract class AbstractEnumerableJsonConverter<TObj, TCollection> :
JsonConverter<TCollection> where TCollection: IEnumerable<TObj>
{
public override TCollection? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var list = new List<TObj>();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
{
break;
}
if (reader.TokenType == JsonTokenType.StartObject)
{
var deserializedObj = default(TObj);
var document = JsonDocument.ParseValue(ref reader);
if (document.RootElement.TryGetProperty("$type", out var typeName))
{
var type = Type.GetType(typeName.GetString());
if (type.IsAssignableTo(typeof(IDbo)))
deserializedObj = (TObj)JsonSerializer.Deserialize(document.RootElement.GetProperty("$value").GetRawText(), type, options);
}
deserializedObj ??= (TObj)JsonSerializer.Deserialize(document.RootElement.ToString(), typeof(TObj), options);
list.Add(deserializedObj);
}
}
var ret = (IList)Activator.CreateInstance(typeToConvert, list.Count);
if (ret.Count == 0)
for (var i = 0; i < list.Count; ++i)
ret.Add(list[i]);
else
for (var i = 0; i < ret.Count; ++i)
ret[i] = list[i];
return (TCollection)ret;
}
public override void Write(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options)
{
writer.WriteStartArray();
foreach (var obj in value)
{
if( obj.GetType() != typeof(TObj))
{
writer.WriteStartObject();
writer.WriteString("$type", obj.GetType().FullName);
writer.WritePropertyName("$value");
JsonSerializer.Serialize(writer, obj, options); //<---- The state is lost so we can no more check cycles**
writer.WriteEndObject();
}
else
JsonSerializer.Serialize(writer, obj, options); //<---- The state is lost so we can no more check cycles**
}
writer.WriteEndArray();
}
} I have tried to do something like #21777 (comment) , but it seems that IgnoreCycles relies on a lot of things that are internal and resides in the Core code ( like PushReferenceForCycleDetection that maybe would help if it's protected and not internal https://github.com/dotnet/runtime/blob/01b7e73cd378145264a7cb7a09365b41ed42b240/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/IgnoreReferenceResolver.cs#L23 ). Is there any way I can get around these cycles? (I have also tried to serialize the properties of the object one by one to avoid calling JsonSerializer.Serialize, but the code to enumerate one object's properties is also internal ) |
When I use a simple JsonConverter, with Reference handler equal to Preserve to serialize cyclic objects, the $id property added to each object is not unique, in this example "$id":1 exists two times :
System.Text.Json : 5.0.0
Document Details
⚠ Do not edit this section. It is required for docs.microsoft.com ➟ GitHub issue linking.
The text was updated successfully, but these errors were encountered: