Skip to content

Commit 6e521d2

Browse files
authored
feat: Support JSON Serialize for Value (#529)
* feat: add ValueJsonConverter to support STJ JSON Serialize Signed-off-by: Weihan Li <[email protected]> * test: add test case Signed-off-by: Weihan Li <[email protected]> * style: apply dotnet-format Signed-off-by: Weihan Li <[email protected]> * test: update test cases Signed-off-by: Weihan Li <[email protected]> * test: update deserialize for int/Datetime Signed-off-by: Weihan Li <[email protected]> * style: apply dotnet format Signed-off-by: Weihan Li <[email protected]> * refactor: update DateTime handling Signed-off-by: Weihan Li <[email protected]> * fix: fix build Signed-off-by: Weihan Li <[email protected]> * test: update test case to cover nested Structure and beautify JSON test data Signed-off-by: Weihan Li <[email protected]> * refactor: update double/int value serialize Signed-off-by: Weihan Li <[email protected]> * include boundaries Signed-off-by: Weihan Li <[email protected]> * update double test case data Signed-off-by: Weihan Li <[email protected]> * test: Update StructureTests double test case Signed-off-by: Weihan Li <[email protected]> * refactor: simplify write number value for ValueJsonConverter Signed-off-by: Weihan Li <[email protected]> --------- Signed-off-by: Weihan Li <[email protected]>
1 parent a0ae014 commit 6e521d2

File tree

5 files changed

+193
-1
lines changed

5 files changed

+193
-1
lines changed

Directory.Packages.props

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
<PackageVersion Include="System.Collections.Immutable" Version="$(MicrosoftExtensionsVersion)" />
2424
<PackageVersion Include="System.Diagnostics.DiagnosticSource"
2525
Version="$(MicrosoftExtensionsVersion)" />
26+
<PackageVersion Include="System.Text.Json"
27+
Version="8.0.5" />
2628
<PackageVersion Include="System.Threading.Channels" Version="$(MicrosoftExtensionsVersion)" />
2729
<PackageVersion Include="System.ValueTuple" Version="4.6.1" />
2830
</ItemGroup>
@@ -48,4 +50,4 @@
4850
<PackageVersion Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" />
4951
</ItemGroup>
5052

51-
</Project>
53+
</Project>

src/OpenFeature/Model/Value.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
using System.Collections.Immutable;
2+
using System.Text.Json.Serialization;
23

34
namespace OpenFeature.Model;
45

56
/// <summary>
67
/// Values serve as a return type for provider objects. Providers may deal in JSON, protobuf, XML or some other data-interchange format.
78
/// This intermediate representation provides a good medium of exchange.
89
/// </summary>
10+
[JsonConverter(typeof(ValueJsonConverter))]
911
public sealed class Value : IEquatable<Value>
1012
{
1113
private readonly object? _innerValue;
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
using System.Diagnostics;
2+
using System.Text.Json;
3+
using System.Text.Json.Serialization;
4+
5+
namespace OpenFeature.Model;
6+
7+
internal sealed class ValueJsonConverter : JsonConverter<Value>
8+
{
9+
public override void Write(Utf8JsonWriter writer, Value value, JsonSerializerOptions options) =>
10+
WriteJsonValue(value, writer);
11+
12+
public override Value Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
13+
ReadJsonValue(ref reader);
14+
15+
private static void WriteJsonValue(Value value, Utf8JsonWriter writer)
16+
{
17+
if (value.IsNull)
18+
{
19+
writer.WriteNullValue();
20+
return;
21+
}
22+
23+
if (value.IsBoolean)
24+
{
25+
writer.WriteBooleanValue(value.AsBoolean.GetValueOrDefault());
26+
return;
27+
}
28+
29+
if (value.IsNumber)
30+
{
31+
writer.WriteNumberValue(value.AsDouble!.Value);
32+
return;
33+
}
34+
35+
if (value.IsString)
36+
{
37+
writer.WriteStringValue(value.AsString);
38+
return;
39+
}
40+
41+
if (value.IsDateTime)
42+
{
43+
writer.WriteStringValue(value.AsDateTime!.Value);
44+
return;
45+
}
46+
47+
if (value.IsList)
48+
{
49+
writer.WriteStartArray();
50+
51+
foreach (var item in value.AsList ?? [])
52+
{
53+
WriteJsonValue(item, writer);
54+
}
55+
56+
writer.WriteEndArray();
57+
return;
58+
}
59+
60+
if (value.IsStructure)
61+
{
62+
writer.WriteStartObject();
63+
64+
var dic = value.AsStructure?.AsDictionary();
65+
if (dic is { Count: > 0 })
66+
{
67+
foreach (var pair in dic)
68+
{
69+
writer.WritePropertyName(pair.Key);
70+
WriteJsonValue(pair.Value, writer);
71+
}
72+
}
73+
74+
writer.WriteEndObject();
75+
}
76+
}
77+
78+
private static Value ReadJsonValue(ref Utf8JsonReader reader)
79+
{
80+
switch (reader.TokenType)
81+
{
82+
case JsonTokenType.True:
83+
return new(true);
84+
case JsonTokenType.False:
85+
return new(false);
86+
case JsonTokenType.Number:
87+
if (reader.TryGetInt32(out var intVal))
88+
return new(intVal);
89+
90+
return new(reader.GetDouble());
91+
case JsonTokenType.String:
92+
if (reader.TryGetDateTime(out var dateTime))
93+
return new(dateTime);
94+
95+
return new(reader.GetString()!);
96+
case JsonTokenType.StartArray:
97+
var list = new List<Value>();
98+
while (reader.Read())
99+
{
100+
if (reader.TokenType == JsonTokenType.EndArray)
101+
{
102+
break;
103+
}
104+
list.Add(ReadJsonValue(ref reader));
105+
}
106+
return new(list);
107+
case JsonTokenType.StartObject:
108+
var objectBuilder = Structure.Builder();
109+
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
110+
{
111+
var name = reader.GetString();
112+
Debug.Assert(name is not null);
113+
reader.Read();
114+
objectBuilder.Set(name!, ReadJsonValue(ref reader));
115+
}
116+
return new(objectBuilder.Build());
117+
118+
default:
119+
return new();
120+
}
121+
}
122+
}
123+

src/OpenFeature/OpenFeature.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<PackageReference Include="Microsoft.Bcl.HashCode" Condition="'$(TargetFramework)' == 'net462' or '$(TargetFramework)' == 'netstandard2.0'" />
1212
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
1313
<PackageReference Include="System.Diagnostics.DiagnosticSource" Condition="'$(TargetFramework)' == 'net462' or '$(TargetFramework)' == 'netstandard2.0'" />
14+
<PackageReference Include="System.Text.Json" Condition="'$(TargetFramework)' == 'net462' or '$(TargetFramework)' == 'netstandard2.0'" />
1415
</ItemGroup>
1516

1617
<ItemGroup>

test/OpenFeature.Tests/StructureTests.cs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System.Collections.Immutable;
2+
using System.Text.Json;
3+
using System.Text.Json.Nodes;
24
using OpenFeature.Model;
35

46
namespace OpenFeature.Tests;
@@ -113,4 +115,66 @@ public void GetEnumerator_Should_Return_Enumerator()
113115
enumerator.MoveNext();
114116
Assert.Equal(VAL, enumerator.Current.Value.AsString);
115117
}
118+
119+
[Theory]
120+
[MemberData(nameof(JsonSerializeTestData))]
121+
public void JsonSerializeTest(Value value, string expectedJson)
122+
{
123+
var serializedJsonNode = JsonSerializer.SerializeToNode(value);
124+
var expectJsonNode = JsonNode.Parse(expectedJson);
125+
Assert.True(JsonNode.DeepEquals(expectJsonNode, serializedJsonNode));
126+
}
127+
128+
[Theory]
129+
[MemberData(nameof(JsonSerializeTestData))]
130+
public void JsonDeserializeTest(Value value, string expectedJson)
131+
{
132+
var serializedJsonNode = JsonSerializer.SerializeToNode(value);
133+
var expectValue = JsonSerializer.Deserialize<Value>(expectedJson);
134+
var expectJsonNode = JsonSerializer.SerializeToNode(expectValue);
135+
Assert.True(JsonNode.DeepEquals(expectJsonNode, serializedJsonNode));
136+
}
137+
138+
public static IEnumerable<object[]> JsonSerializeTestData()
139+
{
140+
yield return [new Value("test"), "\"test\""];
141+
yield return [new Value(1), "1"];
142+
yield return [new Value(1.2), "1.2"];
143+
yield return [new Value(int.MaxValue + 1.0), "2147483648"];
144+
yield return [new Value(true), "true"];
145+
yield return [new Value(false), "false"];
146+
yield return
147+
[
148+
new Value(Structure.Builder()
149+
.Set("name", "Alice")
150+
.Set("age", 16)
151+
.Set("isMale", false)
152+
.Set("bio", new Value())
153+
.Set("bornAt", new DateTime(2000, 1, 1))
154+
.Set("tags", new Value([new Value("girl"), new Value("beauty")]))
155+
.Set("job", Structure.Builder()
156+
.Set("title", "Software Engineer")
157+
.Set("grade", "Senior")
158+
.Build())
159+
.Build()
160+
),
161+
"""
162+
{
163+
"name": "Alice",
164+
"age": 16,
165+
"isMale": false,
166+
"bio": null,
167+
"bornAt": "2000-01-01T00:00:00",
168+
"tags": [
169+
"girl",
170+
"beauty"
171+
],
172+
"job": {
173+
"title": "Software Engineer",
174+
"grade": "Senior"
175+
}
176+
}
177+
"""
178+
];
179+
}
116180
}

0 commit comments

Comments
 (0)