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
EnumMember
will 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
JsonPropertyName
will 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
JsonStringEnumConverter
with 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
JsonStringEnumConverter
during 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 inJsonStringEnumMemberConverter
nonetheless.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, thedeserializationFailureFallbackValue
option 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
JsonStringEnumMemberConverterOptionsAttribute
is provided to specifyJsonStringEnumMemberConverterOptions
directly 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
JsonStringEnumMemberConverter
s can be registered on anJsonSerializerOptions
instance by using thetargetEnumTypes
parameter.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 TimeSpan
s 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 DateTime
s
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 DateTime
s or DateTimeOffset
s 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);
}
}