Last active
June 2, 2025 19:13
-
-
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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