<PublishAot>true</PublishAot>
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using System.Text.Json.Nodes;
public static partial class JsonExtensions
public static void PopulateObject<T>(ReadOnlySpan<char> json, T value, JsonSerializerContext context) where T : class
ArgumentNullException.ThrowIfNull(context);
PopulateObject(json, value, context, t => context.GetTypeInfo(t));
[RequiresUnreferencedCode("Use the method JsonExtensions.PopulateObject<T>(string json, T value, JsonSerializerContext context) instead")]
[RequiresDynamicCode("Use the method JsonExtensions.PopulateObject<T>(string json, T value, JsonSerializerContext context) instead")]
public static void PopulateObject<T>(ReadOnlySpan<char> json, T value, JsonSerializerOptions? options = default) where T : class
options = options ?? JsonSerializerOptions.Default;
var originalResolver = options.TypeInfoResolver
?? (JsonSerializer.IsReflectionEnabledByDefault
? new DefaultJsonTypeInfoResolver() : throw new JsonException("Reflection-based serialization is disabled, please use an explicit JsonSerializerContext"));
PopulateObject(json, value, originalResolver, t => originalResolver.GetTypeInfo(t, options));
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode",
Justification = "The required warning was emitted for public static void PopulateObject<T>(string json, T value, JsonSerializerOptions? options = default)")]
[UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode",
Justification = "The required warning was emitted for public static void PopulateObject<T>(string json, T value, JsonSerializerOptions? options = default)")]
static void PopulateObject<T>(ReadOnlySpan<char> json, T value, IJsonTypeInfoResolver originalResolver, Func<Type, JsonTypeInfo?> getOriginalTypeInfo) where T : class
ArgumentNullException.ThrowIfNull(value);
ArgumentNullException.ThrowIfNull(originalResolver);
if (value is IList list && list.IsFixedSize)
throw new JsonException($"Fixed size lists of type {value.GetType()} cannot be populated with {nameof(JsonExtensions.PopulateObject)}<{typeof(T).Name}>()");
var declaredType = typeof(T) == typeof(object) ? value.GetType() : typeof(T);
var originalTypeInfo = getOriginalTypeInfo(declaredType);
if (originalTypeInfo == null)
throw new JsonException($"No JsonTypeInfo was generated for type {declaredType}");
if (originalTypeInfo.Kind is JsonTypeInfoKind.None)
throw new JsonException($"Object of type {value.GetType()} (kind {originalTypeInfo.Kind}) cannot be populated with {nameof(JsonExtensions.PopulateObject)}<{typeof(T).Name}>()");
var originalOptions = originalTypeInfo.Options;
JsonSerializerOptions populateOptions = new (originalOptions)
PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate,
TypeInfoResolver = originalResolver
.WithAddedModifier(new RootObjectInjector<T>(value).WithRootObjectCreator)
.WithAddedModifier(PopulateProperties),
populateOptions.MakeReadOnly();
if (populateOptions.GetTypeInfo(declaredType) == null)
throw new JsonException($"Object of type {declaredType} cannot be populated with {nameof(JsonExtensions.PopulateObject)}<{typeof(T).Name}>() using {originalResolver}");
var returnedValue = JsonSerializer.Deserialize(json, declaredType, populateOptions);
if (returnedValue is not null && !object.ReferenceEquals(returnedValue, value))
throw new JsonException($"A different object was returned for {returnedValue}");
throw new JsonException($"Object of type {declaredType} cannot be populated with {nameof(JsonExtensions.PopulateObject)}<{typeof(T).Name}>() using {originalResolver}", ex);
class RootObjectInjector<T> where T : class
public RootObjectInjector(T value) => (this.value, this.valueType) = (value ?? throw new ArgumentNullException(nameof(value)), valueType = value.GetType());
internal void WithRootObjectCreator(JsonTypeInfo typeInfo)
if (valueType.IsAssignableTo(typeInfo.Type))
var oldCreateObject = typeInfo.CreateObject;
typeInfo.CreateObject = () =>
Interlocked.Exchange(ref this.value, null) is {} rootValue
? rootValue : (oldCreateObject is not null ? oldCreateObject() : throw new JsonException($"No default creator exists for {typeInfo.Type}."));
static void PopulateProperties(JsonTypeInfo typeInfo)
if (typeInfo.Kind == JsonTypeInfoKind.Object)
typeInfo.PreferredPropertyObjectCreationHandling ??= JsonObjectCreationHandling.Populate;
public class ComplexModel
public ComplexModel? NestedModel { get; set; }
public List<string> Strings { get; set; } = new();
public Inner Inner { get; set; } = new();
public List<int> Integers { get; set; } = new();
public Dictionary<string, string> Dictionary { get; } = new();
public record ImmutableModel(List<string> Strings, Inner Inner);
public record RecursiveImmutableModel(List<string> Strings, RecursiveImmutableModel? Inner = null);
[JsonSerializable(typeof(Model))]
[JsonSerializable(typeof(ComplexModel))]
[JsonSerializable(typeof(ImmutableModel))]
[JsonSerializable(typeof(RecursiveImmutableModel))]
[JsonSerializable(typeof(List<int>))]
[JsonSerializable(typeof(Dictionary<string, string>))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(IConvertible))]
public partial class MySerializationContext : JsonSerializerContext;
public abstract class JsonSerializerTester
public abstract string Serialize<TValue>(TValue value);
public abstract void PopulateObject<TValue>(string json, TValue value) where TValue : class;
public class ReflectionJsonSerializerTester(JsonSerializerOptions? options) : JsonSerializerTester
public override string Serialize<TValue>(TValue value) => JsonSerializer.Serialize(value, options);
public override void PopulateObject<TValue>(string json, TValue value) where TValue : class =>
JsonExtensions.PopulateObject<TValue>(json, value, options);
public class SourceGenerationsonSerializerTester(JsonSerializerContext context) : JsonSerializerTester
public override string Serialize<TValue>(TValue value) => JsonSerializer.Serialize(value, typeof(TValue), context);
public override void PopulateObject<TValue>(string json, TValue value) where TValue : class =>
JsonExtensions.PopulateObject<TValue>(json, value, context);
public string? Title { get; set; }
public string? Link { get; set; }
public string? Head { get; set; }
public static void Test()
Test(new ReflectionJsonSerializerTester(new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }), true);
Test(new SourceGenerationsonSerializerTester(MySerializationContext.Default), false);
public static void TestModel()
var json1 = """{"Title": "Startpage", "Link": "/index" }""";
var json2 = """{"Head": "Latest news", "Link": "/news" }""";
var options = new JsonSerializerOptions
PropertyNameCaseInsensitive = true,
JsonExtensions.PopulateObject(json1, model, options);
JsonExtensions.PopulateObject(json2, model, options);
var newJson = JsonSerializer.Serialize(model, options);
Assert.That(JsonNode.DeepEquals(JsonNode.Parse(newJson), JsonNode.Parse(expectedJson)));
Console.WriteLine(newJson);
[JsonSerializable(typeof(Model))]
public partial class ModelSerializationContext : JsonSerializerContext;
public static void TestModelSourceGen()
var json1 = """{"Title": "Startpage", "Link": "/index" }""";
var json2 = """{"Head": "Latest news", "Link": "/news" }""";
JsonExtensions.PopulateObject(json1, model, ModelSerializationContext.Default);
JsonExtensions.PopulateObject(json2, model, ModelSerializationContext.Default);
var newJson = JsonSerializer.Serialize(model, ModelSerializationContext.Default.Model);
Assert.That(JsonNode.DeepEquals(JsonNode.Parse(newJson), JsonNode.Parse(expectedJson)));
Console.WriteLine(newJson);
static void Test(JsonSerializerTester tester, bool immutableShouldWork = true)
TestComplexModel(tester);
TestIConvertibleInt(tester);
TestRecursiveImmutable(tester);
var ex = Assert.Throws<JsonException>(() => TestRecursiveImmutable(tester));
Console.WriteLine("Caught expected exception when testing RecursiveImmutableModel: {0}", ex?.Message);
TestWhenCreateObjectIsUsed();
public static void TestModel(JsonSerializerTester tester)
var json1 = """{"title": "Startpage", "link": "/index" }""";
var json2 = """{"head": "Latest news", "link": "/news" }""";
tester.PopulateObject(json1, model);
tester.PopulateObject(json2, model);
var newJson = tester.Serialize(model);
Assert.That(JsonNode.DeepEquals(JsonNode.Parse(newJson), JsonNode.Parse(expectedJson)));
Console.WriteLine(newJson);
static void TestRecursiveImmutable(JsonSerializerTester tester)
Console.WriteLine("Testing {0}:", nameof(RecursiveImmutableModel));
RecursiveImmutableModel original = new(["a", "b"], new(["y", "z"]));
var json = tester.Serialize(original);
RecursiveImmutableModel populated = new(new(), new(new()));
var strings = populated.Strings;
var inner = populated.Inner;
var innerStrings = inner!.Strings;
var innerInner= inner!.Inner;
tester.PopulateObject(json, populated);
Assert.That(strings == populated.Strings);
Assert.That(inner == populated.Inner);
Assert.That(innerStrings == populated.Inner?.Strings);
Assert.That(innerInner == populated.Inner?.Inner);
static void TestImmutable(JsonSerializerTester tester)
Console.WriteLine("Testing {0}:", nameof(ImmutableModel));
var original = new ImmutableModel(["a", "b"], new() { Integers = [1, 2], Dictionary = {{"a", "a"}, {"b", "b" }} });
var json = tester.Serialize(original);
var populated = new ImmutableModel(new(), new());
var strings = populated.Strings;
var inner = populated.Inner;
var innerIntegers = inner.Integers;
var innerDictionary = inner.Dictionary;
tester.PopulateObject(json, populated);
Assert.That(strings == populated.Strings);
Assert.That(inner == populated.Inner);
Assert.That(innerIntegers == populated.Inner?.Integers);
Assert.That(innerDictionary == populated.Inner?.Dictionary);
var json2 = tester.Serialize(populated);
Assert.That(json == json2);
static void TestComplexModel(JsonSerializerTester tester)
Console.WriteLine("Testing {0}:", nameof(ComplexModel));
var original = new ComplexModel
NestedModel = new() { Strings = ["z"] },
original.Inner.Integers.Add(1);
original.Inner.Dictionary.Add("a", "b");
var json = tester.Serialize(original);
var populated = new ComplexModel { NestedModel = new() };
var strings = populated.Strings;
var nested = populated.NestedModel;
var nestedStrings = nested.Strings;
var inner = populated.Inner;
var innerIntegers = inner.Integers;
var innerDictionary = inner.Dictionary;
tester.PopulateObject(json, populated);
Assert.That(strings == populated.Strings);
Assert.That(nested == populated.NestedModel);
Assert.That(nestedStrings == populated.NestedModel?.Strings);
Assert.That(inner == populated.Inner);
Assert.That(innerIntegers == populated.Inner?.Integers);
Assert.That(innerDictionary == populated.Inner?.Dictionary);
var json2 = tester.Serialize(populated);
Assert.That(json == json2);
static void TestDictionary(JsonSerializerTester tester)
Console.WriteLine("Testing Dictionary<string, string>:");
Dictionary<string, string> original = new() { {"a", "a"}, {"b", "b" }};
var json = tester.Serialize(original);
Dictionary<string, string> populated = new();
tester.PopulateObject(json, populated);
Assert.That(populated.Count == original.Count);
Assert.That(populated.SequenceEqual(original));
static void TestList(JsonSerializerTester tester)
Console.WriteLine("Testing List<int>:");
tester.PopulateObject("[0, 1, 2]", list);
Assert.That(list.SequenceEqual([0, 1, 2]));
Console.WriteLine(" Test passed.");
static void TestWhenCreateObjectIsUsed()
if (!JsonSerializer.IsReflectionEnabledByDefault)
Box<int> counter = new();
HashSet<object> returnedValues = new();
var options = new JsonSerializerOptions
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
.WithAddedModifier(JsonExtensions.WithDebugCreator(counter, returnedValues)),
var list = JsonSerializer.Deserialize<List<int>>("[1, 2, 3]", options);
Assert.That(returnedValues.Contains(list!));
var collection = JsonSerializer.Deserialize<ObservableCollection<int>>("[1, 2, 3]", options);
Assert.That(returnedValues.Contains(collection!));
static void TestArray(JsonSerializerTester tester)
Console.WriteLine("Testing int [] array:");
int [] array = new int [3];
var ex = Assert.Throws<JsonException>(() => tester.PopulateObject("[0, 1, 2]", array));
Console.WriteLine(" Failed as expected with message: \"{0}\".", ex?.Message);
static void TestString(JsonSerializerTester tester)
Console.WriteLine("Testing string:");
var ex = Assert.Throws<JsonException>(() => tester.PopulateObject("\"hello\"", "some string"));
Console.WriteLine(" Failed as expected with message: \"{0}\".", ex?.Message);
static void TestInt(JsonSerializerTester tester)
Console.WriteLine("Testing string:");
object obj = default(int);
var ex = Assert.Throws<JsonException>(() => tester.PopulateObject("32", obj));
Console.WriteLine(" Failed as expected with message: \"{0}\".", ex?.Message);
static void TestIConvertibleInt(JsonSerializerTester tester)
Console.WriteLine("Testing string:");
IConvertible obj = default(int);
var ex = Assert.Throws<JsonException>(() => tester.PopulateObject<IConvertible>("32", obj));
Console.WriteLine(" Failed as expected with message: \"{0}\".", ex?.Message);
public class Box<T> where T : struct
public static partial class JsonExtensions
public static Action<JsonTypeInfo> WithDebugCreator(Box<int> counter, HashSet<object> returnedValues) => typeInfo =>
var oldCreateObject = typeInfo.CreateObject;
if (oldCreateObject != null)
typeInfo.CreateObject = () =>
Console.WriteLine($"Custom CreateObject() called for type {typeInfo.Type}, count = {++counter.Value}");
var obj = oldCreateObject();
public static void Main()
Console.WriteLine("Environment version: {0} ({1}), {2}", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription , Environment.Version, Environment.OSVersion);
Console.WriteLine("System.Text.Json version: " + typeof(JsonSerializer).Assembly.FullName);
Console.WriteLine("Failed with unhandled exception: ");