Skip to content

Instantly share code, notes, and snippets.

@bojidar-bg
Last active November 12, 2020 18:51
Show Gist options
  • Save bojidar-bg/8124db51da853384bd03f3fa0ef71a35 to your computer and use it in GitHub Desktop.
Save bojidar-bg/8124db51da853384bd03f3fa0ef71a35 to your computer and use it in GitHub Desktop.
JsonConverter for System.Text.Json with support for immutable classes, fields, and polymorphic types

NOTE: Likely obsoleted by .NET 5: dotnet/runtime#29895

This is a JsonConverter for System.Text.Json which implements three "painfully needed" features for JsonSerializer:

  • Deserializing classes without a parameterless contructor (e.g. immutable classes).
  • Serializing and deserializing public non-static fields. (Works with ValueTuple!)
  • Serializing and deserializing polymorphic types. This is relatively dangerous, and can open up your application to remote code execution attacks if used improperly. Would likely be safer with some kind of whilelist/blacklist of specific types or with some kind of per-field whitelist.

It is licensed under the Unlicense, and comes with no strings attached, not even the requirement to mention the original author. Nevertheless, I have included a link back to this gist in the source code, in case a future maintainer needs to check for updates or issues related to this piece of code.

This piece of code was originally written for the Apocryph project, a consensus network for autonomous agents implemented on top of a modern technology stack. You can read more about it at apocryph.network.

License text:

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.

In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to https://unlicense.org

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
// Via https://gist.github.com/bojidar-bg/8124db51da853384bd03f3fa0ef71a35
public class ObjectParameterConstructorConverter : JsonConverterFactory
{
public bool AllowSubtypes { get; set; } = false;
public string TypeProperty { get; set; } = "$type";
public override bool CanConvert(Type type)
{
return !type.IsArray && !type.IsPrimitive && type != typeof(string) && type != typeof(Guid) && !typeof(IEnumerable).IsAssignableFrom(type) && !type.GetConstructors().Any(c => c.GetParameters().Length == 0);
}
public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options)
{
var convertedType = typeof(ObjectParameterConstructorConverterInner<>).MakeGenericType(type);
return (JsonConverter)Activator.CreateInstance(convertedType, new object[] { this })!;
}
private class ObjectParameterConstructorConverterInner<T> : JsonConverter<T>
{
private readonly ObjectParameterConstructorConverter Factory;
public ObjectParameterConstructorConverterInner(ObjectParameterConstructorConverter factory)
{
Factory = factory;
}
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
Dictionary<string, object?> values = new Dictionary<string, object?>();
Dictionary<string, string> valuesLower = new Dictionary<string, string>();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
break;
}
if (reader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException();
}
var propertyName = reader.GetString();
if (Factory.AllowSubtypes && propertyName == Factory.TypeProperty)
{
var typeName = JsonSerializer.Deserialize<string>(ref reader, options);
var newType = Type.GetType(typeName);
if (newType != null && typeToConvert.IsAssignableFrom(newType))
{
typeToConvert = newType;
}
}
var property = typeToConvert.GetProperty(propertyName);
if (property != null && property.GetCustomAttributes(typeof(JsonIgnoreAttribute), false).Length == 0)
{
var value = JsonSerializer.Deserialize(ref reader, property.PropertyType, options);
values.Add(propertyName, value);
valuesLower[NormalizeName(propertyName)] = propertyName;
continue;
}
var field = typeToConvert.GetField(propertyName);
if (field != null && field.GetCustomAttributes(typeof(JsonIgnoreAttribute), false).Length == 0 && !field.IsStatic)
{
var value = JsonSerializer.Deserialize(ref reader, field.FieldType, options);
values.Add(propertyName, value);
valuesLower[NormalizeName(propertyName)] = propertyName;
continue;
}
reader.Skip();
}
if (reader.TokenType != JsonTokenType.EndObject)
{
throw new JsonException();
}
foreach (var constructor in typeToConvert.GetConstructors())
{
var parameters = constructor.GetParameters();
var matchedParameters = 0;
foreach (var parameter in parameters)
{
if (values.ContainsKey(parameter.Name!) || valuesLower.ContainsKey(NormalizeName(parameter.Name!)))
{
matchedParameters++;
}
else if (!parameter.IsOptional)
{
matchedParameters = -1;
break;
}
}
if (matchedParameters < 0)
{
continue;
}
var arguments = new object?[parameters.Length];
for (var i = 0; i < matchedParameters; i++)
{
if (values.ContainsKey(parameters[i].Name!))
{
arguments[i] = values[parameters[i].Name!];
values.Remove(parameters[i].Name!);
}
else
{
var key = valuesLower[NormalizeName(parameters[i].Name!)];
arguments[i] = values[key];
values.Remove(key);
}
}
for (var i = matchedParameters; i < parameters.Length; i++)
{
arguments[i] = parameters[i].DefaultValue;
}
var result = (T)constructor.Invoke(arguments)!;
foreach (var (key, value) in values)
{
var property = typeToConvert.GetProperty(key);
if (property != null && property.CanWrite)
{
property.SetValue(result, value);
continue;
}
var field = typeToConvert.GetField(key);
if (field != null && !field.IsStatic)
{
field.SetValue(result, value);
}
}
return result;
}
throw new JsonException($"No matching constructor for type {typeToConvert} ({typeof(T)})");
}
private string NormalizeName(string name)
{
return new string(name.Where(char.IsLetterOrDigit).Select(x => char.ToLower(x)).ToArray());
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
writer.WriteStartObject();
var type = typeof(T);
if (value?.GetType() != typeof(T) && Factory.AllowSubtypes)
{
type = value?.GetType()!;
// Note: Might need to use Assembly Qualified Name here
writer.WriteString(Factory.TypeProperty, type.FullName);
}
foreach (var property in type.GetProperties())
{
if (!property.CanRead || property.GetCustomAttributes(typeof(JsonIgnoreAttribute), false).Length > 0 || property.IsSpecialName)
{
continue;
}
writer.WritePropertyName(property.Name);
var propertyValue = property.GetValue(value);
JsonSerializer.Serialize(writer, propertyValue, property.PropertyType, options);
}
foreach (var field in type.GetFields())
{
if (field.IsStatic || field.GetCustomAttributes(typeof(JsonIgnoreAttribute), false).Length > 0)
{
continue;
}
writer.WritePropertyName(field.Name);
var propertyValue = field.GetValue(value);
JsonSerializer.Serialize(writer, propertyValue, field.FieldType, options);
}
writer.WriteEndObject();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment