-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Will JsonSerializer support parsing to existing object? #29538
Comments
cc @steveharter |
From @tig in https://github.com/dotnet/corefx/issues/42633
|
FWIW, here's my scenario just so y'all have an understanding of why I think this feature is important: I'm building an app that uses JSON as a config-file format, ala VS Code, Windows Terminal, etc... All the cool kids are doing this. Like those apps, I have a filesystem watcher that watches the .config file for changes. Like those apps, I'd like all my settings to refresh when this happens. The classes I'm serializing with This all works groovy, except that without Thanks for listening. |
I vote for this one too. If objects' hierarchies are deep with lots of fields/properties; this shall come in handy. Support polymorphic deserialization example looks simple because derived classes only have one property each. I have a custom converter that writes generic object type name along with bounding types as part of the json. During deserialization, it creates correct object using those two values, and rest of the reading is done by |
@reazhaq - this is exactly the scenario we have - I have a custom It then wants to populate the contents of the type it has instantiated, using the default behaviour, otherwise you get into an infinite recursion. It would be nice to be able to do a |
With C#8 nullable types, we get a warning for not initializing a non-nullable list, but |
Our workaround for DTOs is to create an empty constructor (if there isn't one already) and wrap it in:
This will stop the analysis for uninitialized properties. We only use this for DTOs where we have knowledge that something in the system stack has already ensured that the property won't be null when deserialized. The other option is to place the |
Yes, I figured suppressing the warning would be a workaround, but was kind of hopping that it wouldn't be necessary. I wish |
Linking #30258 which is concerned with an option to reuse rather than replace object properties on deserialization. They should be considered together during design and implementation. |
Triage: we want to eventually add this feature but unfortunately it won't fit our schedule for .NET 6, moving to future. |
Closely related to #30258. For .NET 7 we will be prioritizing deserialization on existing collections but not objects. Moving to future. |
By the way, even Microsoft itself needs a And just another IMHO: |
This would be fantastic for reducing memory allocations in my app; I want to re-use target objects to avoid allocaitons / GC pauses. Unless there's another route to achieving this? |
@kierenj Once public using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace Sharp.Serializer
{
public class ReferenceConverter : JsonConverter
{
public override bool CanWrite => false;
public override bool CanConvert(Type typeToConvert)
{
return (typeToConvert.IsClass || typeToConvert.IsInterface) && typeToConvert != typeof(string);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
reader.Read();
if (reader.TokenType is JsonToken.StartObject)
reader.Read();
var id = string.Empty;
var valueId = string.Empty;
if (reader.Value is string and "$id" or "$ref")
{
id = reader.Value as string;
reader.Read();
valueId = reader.Value as string;
reader.Read();
}
if (reader.Value is string and "$type")
{
reader.Read();
reader.Read();
}
var contract = serializer.ContractResolver.ResolveContract(objectType) as JsonObjectContract;
var target = serializer.ReferenceResolver.ResolveReference(null, valueId) ?? RuntimeHelpers.GetUninitializedObject(objectType);
if (id is "$ref")
{
reader.Read();
return target;
}
else
serializer.ReferenceResolver.AddReference(null, valueId, target);
while (reader.TokenType is not JsonToken.EndObject)
{
var propName = (string)reader.Value;
reader.Read();
var p = contract.Properties.GetClosestMatchProperty(propName);
if (p.Ignored)
{
reader.Skip();
reader.Read();
}
else
{
p.ValueProvider.SetValue(target, serializer.Deserialize(reader, p.PropertyType));
//while (depth != reader.Depth && reader.TokenType is not JsonToken.PropertyName)
if (reader.TokenType is not JsonToken.PropertyName)
reader.Read();
}
}
return target;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
} it should be possible to achieve deep EDIT: also i dont think it will work with "special" classes like EDIT2: forgot to add (was in a hurry when writing this) that for this to work custom |
Is there a .NET6 / .NET7 way to deserialize JSON onto an existing object? I'm hoping to be able to provide a JSON snippet that overrides properties on an existing object. |
@TonyValenti built-in no neither on 6 or 7 but with public contract in NET 7 (should be available in next preview or right now if ur willing to build BCL yourself from this repo) you can build it almost trivially yourself (unless you want to support all the knobs in which case it becomes cumbersome and time-consuming but not particularly difficult). |
In .NET 7 you can do that quite easily (however this implementation only overwrites the root object and does not merge children [unlike Newtonsoft.Json]): public static class JsonUtilities
{
internal class PopulateTypeInfoResolver : IJsonTypeInfoResolver
{
private bool _isRootResolved = false;
private readonly object _rootObject;
private readonly Type _rootObjectType;
private readonly IJsonTypeInfoResolver _jsonTypeInfoResolver;
public PopulateTypeInfoResolver(object rootObject, IJsonTypeInfoResolver jsonTypeInfoResolver)
{
_rootObject = rootObject;
_rootObjectType = rootObject.GetType();
_jsonTypeInfoResolver = jsonTypeInfoResolver;
}
public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options)
{
var typeInfo = _jsonTypeInfoResolver.GetTypeInfo(type, options);
if (typeInfo != null && type == _rootObjectType)
{
typeInfo.CreateObject = () =>
{
if (!_isRootResolved)
{
_isRootResolved = true;
return _rootObject;
}
else
{
return Activator.CreateInstance(type)!;
}
};
}
return typeInfo;
}
}
public static void PopulateObject(string json, object obj, JsonSerializerOptions? options = null)
{
var modifiedOptions = options != null ?
new JsonSerializerOptions(options)
{
TypeInfoResolver = new PopulateTypeInfoResolver(obj, options.TypeInfoResolver ?? new DefaultJsonTypeInfoResolver()),
} :
new JsonSerializerOptions
{
TypeInfoResolver = new PopulateTypeInfoResolver(obj, new DefaultJsonTypeInfoResolver()),
};
JsonSerializer.Deserialize(json, obj.GetType(), modifiedOptions);
}
} Tests: public class JsonUtilitiesTests
{
[Fact]
public void SystemTextJson()
{
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
var person = new Person
{
FirstName = "Abc",
Child = new Person
{
FirstName = "Foo"
},
Children =
{
new Person { FirstName = "Xyz" }
}
};
var json = @"{ ""lastName"": ""Def"", ""child"": { ""lastName"": ""Bar"" }, ""children"": [ { ""lastName"": ""mno"" } ] }";
JsonUtilities.PopulateObject(json, person, options);
Assert.Equal("Abc", person.FirstName);
Assert.Equal("Def", person.LastName);
Assert.Null(person.Child.FirstName);
Assert.Equal("Bar", person.Child.LastName);
Assert.Single(person.Children);
Assert.Null(person.Children.First().FirstName);
Assert.Equal("mno", person.Children.First().LastName);
}
}
public class Person
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
public Person? Child { get; set; }
public List<Person> Children { get; set; } = new List<Person>();
} Personally this is what I'd expect from PopulateObject(), if existing children should also be updated then I'd call this method MergeInto() - however I dont know how you'd do that with .NET 7 STJ... |
@RicoSuter that's a cool idea, however one issue I'm seeing is that you're creating a fresh using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
var value = new MyPoco();
JsonSerializerExt.PopulateObject("""{"Value":42}""", typeof(MyPoco), value);
Console.Write(value.Value); // 42
public class MyPoco
{
public int Value { get; set; }
}
public static class JsonSerializerExt
{
// Dynamically attach a JsonSerializerOptions copy that is configured using PopulateTypeInfoResolver
private readonly static ConditionalWeakTable<JsonSerializerOptions, JsonSerializerOptions> s_populateMap = new();
public static void PopulateObject(string json, Type returnType, object destination, JsonSerializerOptions? options = null)
{
options = GetOptionsWithPopulateResolver(options);
PopulateTypeInfoResolver.t_populateObject = destination;
try
{
object? result = JsonSerializer.Deserialize(json, returnType, options);
Debug.Assert(ReferenceEquals(result, destination));
}
finally
{
PopulateTypeInfoResolver.t_populateObject = null;
}
}
private static JsonSerializerOptions GetOptionsWithPopulateResolver(JsonSerializerOptions? options)
{
options ??= JsonSerializerOptions.Default;
if (!s_populateMap.TryGetValue(options, out JsonSerializerOptions? populateResolverOptions))
{
JsonSerializer.Serialize(value: 0, options); // Force a serialization to mark options as read-only
Debug.Assert(options.TypeInfoResolver != null);
populateResolverOptions = new JsonSerializerOptions(options)
{
TypeInfoResolver = new PopulateTypeInfoResolver(options.TypeInfoResolver)
};
s_populateMap.TryAdd(options, populateResolverOptions);
}
Debug.Assert(options.TypeInfoResolver is PopulateTypeInfoResolver);
return populateResolverOptions;
}
private class PopulateTypeInfoResolver : IJsonTypeInfoResolver
{
private readonly IJsonTypeInfoResolver _jsonTypeInfoResolver;
[ThreadStatic]
internal static object? t_populateObject;
public PopulateTypeInfoResolver(IJsonTypeInfoResolver jsonTypeInfoResolver)
{
_jsonTypeInfoResolver = jsonTypeInfoResolver;
}
public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options)
{
var typeInfo = _jsonTypeInfoResolver.GetTypeInfo(type, options);
if (typeInfo != null && typeInfo.Kind != JsonTypeInfoKind.None)
{
Func<object>? defaultCreateObjectDelegate = typeInfo.CreateObject;
typeInfo.CreateObject = () =>
{
object? result = t_populateObject;
if (result != null)
{
// clean up to prevent reuse in recursive scenaria
t_populateObject = null;
}
else
{
// fall back to the default delegate
result = defaultCreateObjectDelegate?.Invoke();
}
return result!;
};
}
return typeInfo;
}
}
} |
Very nice! I created a Benchmark for this to compare it with Newtonsoft. Its faster and uses far less memory. I also created a generic method The Benchmark Results:
The code gist: https://gist.github.com/maxreb/947dd57b159d82aa75ac6943d66679e5 |
Nice! I should point out that this technique would only work with the synchronous serialization methods, thread statics stop working when async methods are being used and you might need to use something like |
My main gripe with this is that STJ should expose this natively - it must have a internal populate method where they actually populate the object created with CreateObject, no? |
There is work happening in this space currently tracked by #78556. I think it might make sense to close this issue in favor of the other one since its proposal already includes |
When I need to call deserializer for the same type inside a long running loop, I prefer to overwrite to existing instance rather than create a new instance every time.
Json.NET
has a feature for it:JsonConvert.PopulateObject(string, object)
(I think the target should be the first argument of it, but it doesn't matter.)
For
System.Text.Json
, its usage might look like this:I found an option
ClassMaterializerStrategy
that might do similar thing, but it was an internal option. Even if it was not an internal, I believe a direct method likeParseTo
would be better.The text was updated successfully, but these errors were encountered: