Let's say I have a type/class "Person" and I would like to write custom serialization for it. Typical answer to this problem would be "write a converter". The thing is, I don't want to go this route -- I would like to write serialization directly into this type "Person", so this class would know how to serialize itself.
How to do it when using System.Text.JSON?
CodePudding user response:
I'm not pretty sure whether if this is what you want. SerializationInfo
is just some kinda K-V collection, so if an object implemements any enumerable like IDictionary
or IDictionary<,>
, the default converter will serialize it as a json object.
You can declare an interface to reduce unrelated methods that have to be implemented in the class
interface IJsonObject : IDictionary
{
int ICollection.Count => 0;
ICollection IDictionary.Keys => null;
//...
}
class Person : IJsonObject
{
// data example
Dictionary<string, object> properties = new()
{
["a"] = 100,
["b"] = "Big"
};
IDictionaryEnumerator IDictionary.GetEnumerator()
=> properties.GetEnumerator();
}
Result:
{"a":100,"b":"Big"}
CodePudding user response:
You're fighting against the API design here; a custom JsonConverter
is going to be the easiest way to take control of your type's serialization. That being said, if for some reason you refuse to write a converter, you could customize your JSON serialization by populating a JsonExtensionDataAttribute
overflow dictionary with your customized JSON contents during serialization via an OnSerializing
callback, then postprocess the overflow dictionary to populate your model via an OnDeserialized
callback. You will also need to prevent default serialization by applying [JsonIgnore]
to all properties that should not be serialized automatically.
For instance, say you have the following model:
public class Model
{
public List<string?>? List { get; set; }
}
And you want it to be serialized like so:
{
"0": "value 0",
"1": "value 1"
}
Then you could define your model as follows:
public class Model : IJsonOnDeserialized, IJsonOnSerializing, IJsonOnSerialized
{
[JsonIgnore] // Prevent default serialization of properties that are serialized as part of the JsonExtensionData dictionary
public List<string?>? List { get; set; }
[JsonExtensionData, JsonInclude]
public Dictionary<string, JsonElement>? Elements { get; private set; }
protected virtual Dictionary<string, JsonElement> SerializeJsonElements(Dictionary<string, JsonElement> elements) =>
List.EmptyIfNull().Select((value, index) => (value, index)).Aggregate(elements, (e, p) => { e.Add(p.index.ToInvariantString(), p.value.ToJsonElement()); return e; } );
protected virtual void DeserializeJsonElements(Dictionary<string, JsonElement>? elements) =>
List = elements?.Select(p => p.Value.Deserialize<string>()).ToList();
void IJsonOnSerializing.OnSerializing() => Elements = SerializeJsonElements(new ());
void IJsonOnSerialized.OnSerialized() => Elements = null; // Elements are no longer needed, so null them out.
void IJsonOnDeserialized.OnDeserialized()
{
DeserializeJsonElements(Elements);
Elements = null;
}
}
public static class JsonExtensions
{
public static IEnumerable<T> EmptyIfNull<T>([System.Diagnostics.CodeAnalysis.AllowNull] this IEnumerable<T> enumerable) => enumerable ?? Enumerable.Empty<T>();
public static JsonElement ToJsonElement<T>(this T value, JsonSerializerOptions? options = default) => JsonSerializer.SerializeToElement(value, options);
public static string ToInvariantString<TFormattable>(this TFormattable input) where TFormattable : IFormattable => Convert.ToString(input, CultureInfo.InvariantCulture) ?? "";
}
Then it will be serialized as required. Notes:
The
JsonExtensionData
dictionary must be public, but may have a private setter if also marked with[JsonInclude]
.The dictionary is nulled out in the
OnDeserialized
callback as well as in anOnSerialized
callback to prevent it from permanently consuming memory. (Thus this trick requires the implementation of three interfaces:IJsonOnSerializing
,IJsonOnSerialized
andIJsonOnDeserialized
.)Derived types may add their own content to the dictionary of custom elements by overriding
SerializeJsonElements()
andDeserializeJsonElements()
.
Demo fiddle #1 here.
As an aside, I would question your assumption that writing a converter is difficult or adds complexity to your library. The converter can be a private, nested type inside your type as long as you apply it direct to the type via [JsonConverter(typeof(TMyType.TInnerConverter))
. For instance, here's a private converter for the Model
type above:
[JsonConverter(typeof(Model.ModelConverter))]
public class Model
{
public List<string?>? List { get; set; }
private class ModelConverter : JsonConverter<Model>
{
public override Model? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
new Model { List = JsonSerializer.Deserialize<Dictionary<string, string?>>(ref reader, options)?.Values.ToList() };
public override void Write(Utf8JsonWriter writer, Model value, JsonSerializerOptions options) =>
JsonSerializer.Serialize(writer, value.List.EmptyIfNull().Select((value, index) => (value, index)).ToDictionary(p => p.index.ToInvariantString(), p => p.value));
}
}
public static class JsonExtensions
{
public static IEnumerable<T> EmptyIfNull<T>([System.Diagnostics.CodeAnalysis.AllowNull] this IEnumerable<T> enumerable) => enumerable ?? Enumerable.Empty<T>();
public static string ToInvariantString<TFormattable>(this TFormattable input) where TFormattable : IFormattable => Convert.ToString(input, CultureInfo.InvariantCulture) ?? "";
}
I think this is simpler than the JsonExtensionData
workaround, and adds no complexity to the public surface of your types.
Demo fiddle #2 here.