Macross.Json.Extensions is a .NET Standard 2.0+ library for augmenting what is provided out of the box by the System.Text.Json & System.Net.Http APIs.
For a list of changes see: CHANGELOG
JsonStringEnumMemberConverter is similar to the official JsonStringEnumConverter but it adds a few features and bug fixes.
-
EnumMemberAttribute Support
When serializing and deserializing an Enum as a string the value specified by
EnumMemberwill be used.[JsonConverter(typeof(JsonStringEnumMemberConverter))] public enum DefinitionType { [EnumMember(Value = "UNKNOWN_DEFINITION_000")] DefinitionUnknown } [TestMethod] public void ExampleTest() { string Json = JsonSerializer.Serialize(DefinitionType.DefinitionUnknown); Assert.AreEqual("\"UNKNOWN_DEFINITION_000\"", Json); DefinitionType ParsedDefinitionType = JsonSerializer.Deserialize<DefinitionType>(Json); Assert.AreEqual(DefinitionType.DefinitionUnknown, ParsedDefinitionType); }
-
JsonPropertyName Support (available for System.Text.Json 5.0.0+, needs field support added with dotnet/runtime#36986)
When serializing and deserializing an Enum as a string the value specified by
JsonPropertyNamewill be used.[JsonConverter(typeof(JsonStringEnumMemberConverter))] public enum DefinitionType { [JsonPropertyName("UNKNOWN_DEFINITION_000")] DefinitionUnknown } [TestMethod] public void ExampleTest() { string Json = JsonSerializer.Serialize(DefinitionType.DefinitionUnknown); Assert.AreEqual("\"UNKNOWN_DEFINITION_000\"", Json); DefinitionType ParsedDefinitionType = JsonSerializer.Deserialize<DefinitionType>(Json); Assert.AreEqual(DefinitionType.DefinitionUnknown, ParsedDefinitionType); }
-
Nullable<Enum> Support
If you try to use the built-in
JsonStringEnumConverterwith a nullable enum you will get an exception. This scenario is supported byJsonStringEnumMemberConverter.public class JsonObject { public int? Value { get; set; } [JsonConverter(typeof(JsonStringEnumMemberConverter))] public DayOfWeek? DayOfWeek { get; set; } } [TestMethod] public void ExampleTest() { string Json = JsonSerializer.Serialize(new JsonObject()); Assert.AreEqual("{\"Value\":null,\"DayOfWeek\":null}", Json); JsonObject ParsedObject = JsonSerializer.Deserialize<JsonObject>(Json); Assert.IsFalse(ParsedObject.DayOfWeek.HasValue); }
-
Naming Policy Deserialization Support
If a custom naming policy is used in conjunction with the built-in
JsonStringEnumConverterduring serialization, and it makes changes bigger than just casing, it won't deserialize back again. This is a rare bug that no one will probably care about, but it is fixed inJsonStringEnumMemberConverternonetheless.private class CustomJsonNamingPolicy : JsonNamingPolicy { public override string ConvertName(string name) => $"_{name}"; } [TestMethod] public void ExampleTest() { JsonSerializerOptions Options = new JsonSerializerOptions(); Options.Converters.Add(new JsonStringEnumMemberConverter(new CustomJsonNamingPolicy())); string Json = JsonSerializer.Serialize(DayOfWeek.Friday, Options); Assert.AreEqual("\"_Friday\"", Json); DayOfWeek ParsedDayOfWeek = JsonSerializer.Deserialize<DayOfWeek>(Json, Options); Assert.AreEqual(DayOfWeek.Friday, ParsedDayOfWeek); }
-
Deserialization Failure Fallback Value
If a json value is received that cannot be converted into something defined on the target enum the default behavior is to throw a
JsonException. If you would prefer to have a default definition returned instead, thedeserializationFailureFallbackValueoption is provided.public enum MyEnum { Unknown = 0, [EnumMember(Value = "value1")] ValidValue = 1 } [TestMethod] public void DeserializationWithFallbackTest() { JsonSerializerOptions Options = new JsonSerializerOptions { Converters = { new JsonStringEnumMemberConverter( new JsonStringEnumMemberConverterOptions( deserializationFailureFallbackValue: MyEnum.Unknown)) } }; MyEnum parsedValue = JsonSerializer.Deserialize<MyEnum>(@"""value99""", Options); Assert.AreEqual(MyEnum.Unknown, parsedValue); }
-
Specifying options declaratively
JsonStringEnumMemberConverterOptionsAttributeis provided to specifyJsonStringEnumMemberConverterOptionsdirectly on the enum type being serialized/deserialized byJsonStringEnumMemberConverter.[JsonStringEnumMemberConverterOptions(deserializationFailureFallbackValue: MyEnum.Unknown)] [JsonConverter(typeof(JsonStringEnumMemberConverter))] public enum MyEnum { Unknown = 0, [EnumMember(Value = "value1")] ValidValue = 1 }
-
Specifying options at run-time
Multiple
JsonStringEnumMemberConverters can be registered on anJsonSerializerOptionsinstance by using thetargetEnumTypesparameter.JsonSerializerOptions options = new JsonSerializerOptions { Converters = { new JsonStringEnumMemberConverter( new JsonStringEnumMemberConverterOptions(deserializationFailureFallbackValue: DayOfWeek.Friday), typeof(DayOfWeek)), new JsonStringEnumMemberConverter( new JsonStringEnumMemberConverterOptions(deserializationFailureFallbackValue: 0), typeof(FlagDefinitions), typeof(EnumDefinition?)), new JsonStringEnumMemberConverter(allowIntegerValues: false) } };
Note: As of 3.0.0-beta1 JsonTimeSpanConverter has been removed.
System.Text.Json has native support for TimeSpan as of .NET6.
Blog: https://blog.macrosssoftware.com/index.php/2020/02/16/system-text-json-timespan-serialization/
System.Text.Json doesn't support TimeSpan [de]serialization at all (see
corefx #38641). It appears to
be slated for .NET 6, but in the meantime
JsonTimeSpanConverter
is provided to add in support for TimeSpan and TimeSpan? for those of us who
need to transport time values in our JSON ahead of the next major release.
Usage is simple, register the JsonTimeSpanConverter on your TimeSpans or via
JsonSerializerOptions.Converters.
TimeSpan values will be transposed using the Constant ("c") Format
Specifier.
public class TestClass
{
[JsonConverter(typeof(JsonTimeSpanConverter))]
public TimeSpan TimeSpan { get; set; }
[JsonConverter(typeof(JsonTimeSpanConverter))]
public TimeSpan? NullableTimeSpan { get; set; }
}Note: As of 3.0.0-beta2 JsonMicrosoftDateTimeConverter follows the DateTime
Wire
Format
specification.
Some of the older Microsoft JSON serialization libraries handle DateTimes
differently than System.Text.Json does. If you are talking to an older API and
it returns JSON like this...
\/Date(1580803200000-0800)\/ or \/Date(1580803200000)\/
...you will get an exception (see runtime
#30776) trying to deserialize
those into DateTimes or DateTimeOffsets with what System.Text.Json provides
out of the box.
JsonMicrosoftDateTimeConverter and JsonMicrosoftDateTimeOffsetConverter are provided to add in support for the legacy Microsoft date format.
public class TestClass
{
[JsonConverter(typeof(JsonMicrosoftDateTimeConverter))]
public DateTime DateTime { get; set; }
[JsonConverter(typeof(JsonMicrosoftDateTimeConverter))]
public DateTime? NullableDateTime { get; set; }
[JsonConverter(typeof(JsonMicrosoftDateTimeOffsetConverter))]
public DateTimeOffset DateTimeOffset { get; set; }
[JsonConverter(typeof(JsonMicrosoftDateTimeOffsetConverter))]
public DateTimeOffset? NullableDateTimeOffset { get; set; }
}JsonIPAddressConverter
and
JsonIPEndPointConverter
are provided to add in support for serialization of the System.Net IPAddress
and IPEndPoint primitives. They will serialize using the ToString logic of
each respective type.
Usage example:
public class TestClass
{
[JsonConverter(typeof(JsonIPAddressConverter))]
public IPAddress IPAddress { get; set; }
[JsonConverter(typeof(JsonIPEndPointConverter))]
public IPEndPoint IPEndPoint { get; set; }
}Serialization output example:
{
"IPv4Address": "127.0.0.1",
"IPv6Address": "::1",
"IPv4EndPoint": "127.0.0.1:443",
"IPv6EndPoint": "[::1]:443"
}Note: As of 3.0.0-beta1 JsonVersionConverter has been removed.
System.Text.Json has native support for Version as of .NET6.
JsonVersionConverter
is provided to add in support for serialization of the System.Version. It will
serialize using the ToString logic of Version.
Blog: https://blog.macrosssoftware.com/index.php/2020/04/02/efficient-posting-of-json-to-request-streams/
- A port to .NET Standard 2.0+ of the old PushStreamContent class.
JsonDelegatedStringConverter
is added to support creating converters from simple ToString/FromString
function pairs that can be defined without the overhead of creating a full
converter.
Usage example:
JsonSerializerOptions options = new JsonSerializerOptions();
options.Converters.Add(
new JsonDelegatedStringConverter<TimeSpan>(
value => TimeSpan.ParseExact(value, "c", CultureInfo.InvariantCulture),
value => value.ToString("c", CultureInfo.InvariantCulture)));
string Json = JsonSerializer.Serialize(new TimeSpan(1, 2, 3), options);
TimeSpan Value = JsonSerializer.Deserialize<TimeSpan>(Json, options);The catch is JsonDelegatedStringConverter cannot be used with
JsonConverterAttribute because C# doesn't support generic attribues. Maybe
someday it will.
To get around that you can derive from JsonDelegatedStringConverter to create
a type without generics, like this:
public class JsonTimeSpanConverter : JsonDelegatedStringConverter<TimeSpan>
{
public JsonTimeSpanConverter()
: base(
value => TimeSpan.ParseExact(value, "c", CultureInfo.InvariantCulture),
value => value.ToString("c", CultureInfo.InvariantCulture))
{
}
}
public class TestClass
{
[JsonConverter(typeof(JsonTimeSpanConverter))]
public TimeSpan TimeSpan { get; set; }
}BUT watch out for Nullable<T> value types when you do that, they won't work
correctly until .NET 5. There is a bug in
System.Text.Json.
The .NET runtime provides the
TypeConverter
class in the System.ComponentModel namespace as a generic mechanism for
converting between different types at runtime. TypeConverter is applied to a
target type using an attribute
decoration
pattern, much like JsonConverter, but it is not supported by the
System.Text.Json engine (as of .NET 5).
The JsonTypeConverterAdapter is provided to add support for using a
TypeConverter to [de]serialize a given type through System.Text.Json. Note:
The TypeConverter being used must support string as a "to" destination &
"from" source.
[JsonConverter(typeof(JsonTypeConverterAdapter))]
[TypeConverter(typeof(MyCustomTypeConverter))]
public class MyClass
{
public string PropertyA { get; }
public string PropertyB { get; }
internal MyClass(string propertyA, string propertyB)
{
PropertyA = propertyA;
PropertyB = propertyB;
}
}
public class MyCustomTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) =>
sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string str)
{
string[] data = str.Split('|');
return new MyClass(data[0], data[1]);
}
return base.ConvertFrom(context, culture, value);
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
=> destinationType == typeof(string) || base.CanConvertTo(context, destinationType);
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
if (value is MyClass myClass && destinationType == typeof(string))
{
return $"{myClass.PropertyA}|{myClass.PropertyB}";
}
return base.ConvertTo(context, culture, value, destinationType);
}
}