Skip to content

Commit 9b42e4f

Browse files
authored
add support for System.Text.Json (#102)
1 parent 718b308 commit 9b42e4f

File tree

9 files changed

+459
-0
lines changed

9 files changed

+459
-0
lines changed

SmartEnum.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{FA199ECB-5F2
5050
EndProject
5151
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{5952C160-ABAA-4302-9150-0928E82545B1}"
5252
EndProject
53+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartEnum.SystemTextJson", "src\SmartEnum.SystemTextJson\SmartEnum.SystemTextJson.csproj", "{A7F1C0E7-F641-4800-98DA-FA7A7B7B76E9}"
54+
EndProject
55+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartEnum.SystemTextJson.UnitTests", "test\SmartEnum.SystemTextJson.UnitTests\SmartEnum.SystemTextJson.UnitTests.csproj", "{3EBD6CA5-CF2C-4350-922E-CEE2AE01445A}"
56+
EndProject
5357
Global
5458
GlobalSection(SolutionConfigurationPlatforms) = preSolution
5559
Debug|Any CPU = Debug|Any CPU
@@ -108,6 +112,14 @@ Global
108112
{66EF3D1A-32AE-4B28-BC3E-7441615722A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
109113
{66EF3D1A-32AE-4B28-BC3E-7441615722A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
110114
{66EF3D1A-32AE-4B28-BC3E-7441615722A9}.Release|Any CPU.Build.0 = Release|Any CPU
115+
{A7F1C0E7-F641-4800-98DA-FA7A7B7B76E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
116+
{A7F1C0E7-F641-4800-98DA-FA7A7B7B76E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
117+
{A7F1C0E7-F641-4800-98DA-FA7A7B7B76E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
118+
{A7F1C0E7-F641-4800-98DA-FA7A7B7B76E9}.Release|Any CPU.Build.0 = Release|Any CPU
119+
{3EBD6CA5-CF2C-4350-922E-CEE2AE01445A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
120+
{3EBD6CA5-CF2C-4350-922E-CEE2AE01445A}.Debug|Any CPU.Build.0 = Debug|Any CPU
121+
{3EBD6CA5-CF2C-4350-922E-CEE2AE01445A}.Release|Any CPU.ActiveCfg = Release|Any CPU
122+
{3EBD6CA5-CF2C-4350-922E-CEE2AE01445A}.Release|Any CPU.Build.0 = Release|Any CPU
111123
EndGlobalSection
112124
GlobalSection(SolutionProperties) = preSolution
113125
HideSolutionNode = FALSE
@@ -126,6 +138,8 @@ Global
126138
{C65837A3-1AB1-4AD4-9D9E-C3FF437A5714} = {79268877-BBEF-4DE2-B8D9-697F21933159}
127139
{8FC7B9E5-B651-42BC-B21B-B45F0C8A239B} = {FA199ECB-5F29-442A-AAC6-91DBCB7A5A04}
128140
{66EF3D1A-32AE-4B28-BC3E-7441615722A9} = {79268877-BBEF-4DE2-B8D9-697F21933159}
141+
{A7F1C0E7-F641-4800-98DA-FA7A7B7B76E9} = {FA199ECB-5F29-442A-AAC6-91DBCB7A5A04}
142+
{3EBD6CA5-CF2C-4350-922E-CEE2AE01445A} = {79268877-BBEF-4DE2-B8D9-697F21933159}
129143
EndGlobalSection
130144
GlobalSection(ExtensibilityGlobals) = postSolution
131145
SolutionGuid = {46896DE3-41B8-442F-A6FB-6AC9F11CCBCE}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFrameworks>netstandard2.0;</TargetFrameworks>
4+
<PackageId>Ardalis.SmartEnum.SystemTextJson</PackageId>
5+
<Title>Ardalis.SmartEnum.SystemTextJson</Title>
6+
<Company>Ardalis.com</Company>
7+
<Summary>System.Text.Json (de)serialization support for Ardalis.SmartEnum.</Summary>
8+
<PackageTags>enum;smartenum;netstandard2.0;json;system.text.json;converter</PackageTags>
9+
<PackageIcon>icon.png</PackageIcon>
10+
<Version>1.0.0</Version>
11+
<AssemblyName>Ardalis.SmartEnum.SystemTextJson</AssemblyName>
12+
<RootNamespace>Ardalis.SmartEnum.SystemTextJson</RootNamespace>
13+
<Features>strict</Features>
14+
<PublishRepositoryUrl>true</PublishRepositoryUrl>
15+
</PropertyGroup>
16+
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
17+
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
18+
</PropertyGroup>
19+
<ItemGroup>
20+
<PackageReference Include="Ardalis.SmartEnum" Version="1.0.11" />
21+
<PackageReference Include="System.Text.Json" Version="5.0.0" />
22+
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0-*" PrivateAssets="All" />
23+
<PackageReference Include="SonarAnalyzer.CSharp" Version="7.9.0.7583" PrivateAssets="All" />
24+
</ItemGroup>
25+
<ItemGroup>
26+
<None Include="icon.png" Pack="true" Visible="false" PackagePath="" />
27+
</ItemGroup>
28+
</Project>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
namespace Ardalis.SmartEnum.SystemTextJson
2+
{
3+
using System;
4+
using System.Text.Json;
5+
using System.Text.Json.Serialization;
6+
7+
public class SmartEnumNameConverter<TEnum, TValue> : JsonConverter<TEnum>
8+
where TEnum : SmartEnum<TEnum, TValue>
9+
where TValue : IEquatable<TValue>, IComparable<TValue>, IConvertible
10+
{
11+
public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
12+
{
13+
switch (reader.TokenType)
14+
{
15+
case JsonTokenType.String:
16+
return GetFromName(reader.GetString());
17+
18+
default:
19+
throw new JsonException($"Unexpected token {reader.TokenType} when parsing a smart enum.");
20+
}
21+
}
22+
23+
public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options)
24+
{
25+
if (value == null)
26+
writer.WriteNullValue();
27+
else
28+
writer.WriteStringValue(value.Name.ToString());
29+
}
30+
31+
private TEnum GetFromName(string name)
32+
{
33+
try
34+
{
35+
return SmartEnum<TEnum, TValue>.FromName(name, false);
36+
}
37+
catch (Exception ex)
38+
{
39+
throw new JsonException($"Error converting value '{name}' to a smart enum.", ex);
40+
}
41+
}
42+
}
43+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
namespace Ardalis.SmartEnum.SystemTextJson
2+
{
3+
using System;
4+
using System.Text.Json;
5+
using System.Text.Json.Serialization;
6+
7+
public class SmartEnumValueConverter<TEnum, TValue> : JsonConverter<TEnum>
8+
where TEnum : SmartEnum<TEnum, TValue>
9+
where TValue : IEquatable<TValue>, IComparable<TValue>, IConvertible
10+
{
11+
public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
12+
{
13+
if (reader.TokenType == JsonTokenType.Null)
14+
return null;
15+
16+
return GetFromValue(ReadValue(ref reader));
17+
}
18+
19+
public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options)
20+
{
21+
if (value == null)
22+
writer.WriteNullValue();
23+
else if (typeof(TValue) == typeof(bool))
24+
writer.WriteBooleanValue((bool)(object)value.Value);
25+
else if (typeof(TValue) == typeof(short))
26+
writer.WriteNumberValue((int)(short)(object)value.Value);
27+
else if (typeof(TValue) == typeof(int))
28+
writer.WriteNumberValue((int)(object)value.Value);
29+
else if (typeof(TValue) == typeof(double))
30+
writer.WriteNumberValue((double)(object)value.Value);
31+
else if (typeof(TValue) == typeof(decimal))
32+
writer.WriteNumberValue((decimal)(object)value.Value);
33+
else if (typeof(TValue) == typeof(ulong))
34+
writer.WriteNumberValue((ulong)(object)value.Value);
35+
else if (typeof(TValue) == typeof(uint))
36+
writer.WriteNumberValue((uint)(object)value.Value);
37+
else if (typeof(TValue) == typeof(float))
38+
writer.WriteNumberValue((float)(object)value.Value);
39+
else if (typeof(TValue) == typeof(long))
40+
writer.WriteNumberValue((long)(object)value.Value);
41+
else
42+
writer.WriteStringValue(value.Value.ToString());
43+
}
44+
45+
private TEnum GetFromValue(TValue value)
46+
{
47+
try
48+
{
49+
return SmartEnum<TEnum, TValue>.FromValue(value);
50+
}
51+
catch (Exception ex)
52+
{
53+
throw new JsonException($"Error converting value '{value}' to a smart enum.", ex);
54+
}
55+
}
56+
57+
private TValue ReadValue(ref Utf8JsonReader reader)
58+
{
59+
if (typeof(TValue) == typeof(bool))
60+
return (TValue)(object)reader.GetBoolean();
61+
if (typeof(TValue) == typeof(byte))
62+
return (TValue)(object)reader.GetByte();
63+
if (typeof(TValue) == typeof(sbyte))
64+
return (TValue)(object)reader.GetSByte();
65+
if (typeof(TValue) == typeof(short))
66+
return (TValue)(object)reader.GetInt16();
67+
if (typeof(TValue) == typeof(ushort))
68+
return (TValue)(object)reader.GetUInt16();
69+
if (typeof(TValue) == typeof(int))
70+
return (TValue)(object)reader.GetInt32();
71+
if (typeof(TValue) == typeof(uint))
72+
return (TValue)(object)reader.GetUInt32();
73+
if (typeof(TValue) == typeof(long))
74+
return (TValue)(object)reader.GetInt64();
75+
if (typeof(TValue) == typeof(ulong))
76+
return (TValue)(object)reader.GetUInt64();
77+
if (typeof(TValue) == typeof(float))
78+
return (TValue)(object)reader.GetSingle();
79+
if (typeof(TValue) == typeof(double))
80+
return (TValue)(object)reader.GetDouble();
81+
if (typeof(TValue) == typeof(string))
82+
return (TValue)(object)reader.GetString();
83+
84+
throw new ArgumentOutOfRangeException(typeof(TValue).ToString(), $"{typeof(TValue).Name} is not supported.");
85+
}
86+
}
87+
}
7.7 KB
Loading
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netcoreapp3.1</TargetFramework>
5+
</PropertyGroup>
6+
7+
<ItemGroup>
8+
<ProjectReference Include="..\..\src\SmartEnum.SystemTextJson\SmartEnum.SystemTextJson.csproj" />
9+
</ItemGroup>
10+
11+
</Project>
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
namespace Ardalis.SmartEnum.SystemTextJson.UnitTests
2+
{
3+
using FluentAssertions;
4+
using System;
5+
using System.Text.Json;
6+
using System.Text.Json.Serialization;
7+
using Xunit;
8+
9+
public class SmartEnumNameConverterTests
10+
{
11+
public class TestClass
12+
{
13+
[JsonConverter(typeof(SmartEnumNameConverter<TestEnumBoolean, bool>))]
14+
public TestEnumBoolean Bool { get; set; }
15+
16+
[JsonConverter(typeof(SmartEnumNameConverter<TestEnumInt16, short>))]
17+
public TestEnumInt16 Int16 { get; set; }
18+
19+
[JsonConverter(typeof(SmartEnumNameConverter<TestEnumInt32, int>))]
20+
public TestEnumInt32 Int32 { get; set; }
21+
22+
[JsonConverter(typeof(SmartEnumNameConverter<TestEnumDouble, double>))]
23+
public TestEnumDouble Double { get; set; }
24+
25+
[JsonConverter(typeof(SmartEnumNameConverter<TestEnumString, string>))]
26+
public TestEnumString String { get; set; }
27+
}
28+
29+
static readonly TestClass TestInstance = new TestClass {
30+
Bool = TestEnumBoolean.Instance,
31+
Int16 = TestEnumInt16.Instance,
32+
Int32 = TestEnumInt32.Instance,
33+
Double = TestEnumDouble.Instance,
34+
String = TestEnumString.Instance,
35+
};
36+
37+
static readonly string JsonString =
38+
@"{
39+
""Bool"": ""Instance"",
40+
""Int16"": ""Instance"",
41+
""Int32"": ""Instance"",
42+
""Double"": ""Instance"",
43+
""String"": ""Instance""
44+
}";
45+
46+
[Fact]
47+
public void SerializesNames()
48+
{
49+
var json = JsonSerializer.Serialize(TestInstance, new JsonSerializerOptions { WriteIndented = true });
50+
51+
json.Should().Be(JsonString);
52+
}
53+
54+
[Fact]
55+
public void DeserializesNames()
56+
{
57+
var obj = JsonSerializer.Deserialize<TestClass>(JsonString);
58+
59+
obj.Bool.Should().BeSameAs(TestEnumBoolean.Instance);
60+
obj.Int16.Should().BeSameAs(TestEnumInt16.Instance);
61+
obj.Int32.Should().BeSameAs(TestEnumInt32.Instance);
62+
obj.Double.Should().BeSameAs(TestEnumDouble.Instance);
63+
obj.String.Should().BeSameAs(TestEnumString.Instance);
64+
}
65+
66+
[Fact]
67+
public void DeserializesNullByDefault()
68+
{
69+
string json = @"{}";
70+
71+
var obj = JsonSerializer.Deserialize<TestClass>(json);
72+
73+
obj.Bool.Should().BeNull();
74+
obj.Int16.Should().BeNull();
75+
obj.Int32.Should().BeNull();
76+
obj.Double.Should().BeNull();
77+
obj.String.Should().BeNull();
78+
}
79+
80+
[Fact]
81+
public void DeserializeThrowsWhenNotFound()
82+
{
83+
string json = @"{ ""Bool"": ""Not Found"" }";
84+
85+
Action act = () => JsonSerializer.Deserialize<TestClass>(json);
86+
87+
act.Should()
88+
.Throw<JsonException>()
89+
.WithMessage($@"Error converting value 'Not Found' to a smart enum.")
90+
.WithInnerException<SmartEnumNotFoundException>()
91+
.WithMessage($@"No {nameof(TestEnumBoolean)} with Name ""Not Found"" found.");
92+
}
93+
94+
95+
// JsonSerializer doesn't call the converter on null values
96+
//[Fact]
97+
//public void DeserializeThrowsWhenNull()
98+
//{
99+
// string json = @"{ ""Bool"": null }";
100+
101+
// Action act = () => JsonSerializer.Deserialize<TestClass>(json);
102+
103+
// act.Should()
104+
// .Throw<JsonException>()
105+
// .WithMessage($@"Unexpected token Null when parsing a smart enum.");
106+
//}
107+
}
108+
}

0 commit comments

Comments
 (0)