Skip to content

Instantly share code, notes, and snippets.

@NickStrupat
Last active June 2, 2025 19:13
Show Gist options
  • Save NickStrupat/365dba5b85dc77051405d51281488242 to your computer and use it in GitHub Desktop.
Save NickStrupat/365dba5b85dc77051405d51281488242 to your computer and use it in GitHub Desktop.
Accept partial JSON objects that can patch your model. All JsonSerializerOptions are respected (pretty sure)
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
namespace JsonPartial;
[JsonConverter(typeof(PartialConverterFactory))]
public sealed class Partial<T> : IPartial where T : notnull
{
private readonly Action<T> apply;
internal Partial(Action<T> apply) => this.apply = apply;
public T ApplyTo(T target) { apply(target); return target; }
}
file interface IPartial;
internal sealed class PartialConverterFactory : JsonConverterFactory
{
public override Boolean CanConvert(Type typeToConvert) => typeToConvert.IsAssignableTo(typeof(IPartial));
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var type = typeToConvert.GetGenericArguments()[0];
var converterType = typeof(Converter<>).MakeGenericType(type);
var converter = (JsonConverter?)Activator.CreateInstance(converterType);
return converter;
}
private sealed class Converter<T> : JsonConverter<Partial<T>> where T : notnull
{
public override Partial<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var jsonElement = JsonSerializer.Deserialize<JsonElement>(ref reader, options);
var value = jsonElement.Deserialize<T>(WithoutRequiredProperties(options));
var jpis = options.GetTypeInfo(typeof(T)).Properties.ToDictionary(p => p.Name);
Action<T> apply = delegate {};
foreach (var jp in jsonElement.EnumerateObject())
if (jpis.TryGetValue(jp.Name, out var jpi) && jpi is { Set: {} set, AttributeProvider: PropertyInfo pi})
apply += x => set(x, pi.GetValue(value));
return new Partial<T>(apply);
}
public override void Write(Utf8JsonWriter writer, Partial<T> value, JsonSerializerOptions options) =>
throw new NotSupportedException(
$"{typeof(Partial<>).Name} cannot be serialized. It is only for deserialization."
);
}
private static JsonSerializerOptions WithoutRequiredProperties(JsonSerializerOptions options)
{
var resolver = new DefaultJsonTypeInfoResolver { Modifiers = { MakePropertiesOptional } };
return new JsonSerializerOptions(options) { TypeInfoResolver = resolver };
static void MakePropertiesOptional(JsonTypeInfo typeInfo)
{
if (typeInfo.Kind == JsonTypeInfoKind.Object)
foreach (var jpi in typeInfo.Properties)
jpi.IsRequired = false;
}
}
}
using System.Text.Json;
using JsonPartial;
var person = new Person
{
FirstName = "Jimothy",
LastName = "Jillikerson",
BirthDate = new DateOnly(1873, 4, 21)
};
var json = """
{
"firstName": "Jim"
}
""";
var partialPerson = JsonSerializer.Deserialize<Partial<Person>>(json, JsonSerializerOptions.Web)!;
PrintPerson(person);
partialPerson.ApplyTo(person);
PrintPerson(person);
static void PrintPerson(Person p) =>
Console.WriteLine(JsonSerializer.Serialize(p, new JsonSerializerOptions { WriteIndented = true}));
public class Person
{
public required String FirstName { get; set; }
public required String LastName { get; set; }
public DateOnly BirthDate { get; set; }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment