Skip to content

Commit 5cea4c9

Browse files
committed
Support for "UseStringProtoEnumValueNames" option
1 parent 79ff8a5 commit 5cea4c9

12 files changed

Lines changed: 200 additions & 25 deletions

benchmark/Protobuf.System.Text.Json.Benchmark.Shared/NewtonsoftProtobufJsonConverter.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ public NewtonsoftProtobufJsonConverter(int messageRecursionLimit = 3, bool forma
2525
_jsonFormatter = new JsonFormatter(formatterSettings);
2626
}
2727

28-
public override void WriteJson(JsonWriter writer, object? value, Newtonsoft.Json.JsonSerializer serializer)
28+
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
2929
{
3030
writer.WriteRawValue(_jsonFormatter.Format((IMessage?) value));
3131
}
3232

33-
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, Newtonsoft.Json.JsonSerializer serializer)
33+
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
3434
{
3535
return _jsonParser.Parse(JObject.Load(reader).ToString(), ExtractMessageDescriptor(objectType));
3636
}

src/Protobuf.System.Text.Json/FieldInfo.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ namespace Protobuf.System.Text.Json;
66
internal class FieldInfo
77
{
88
public IFieldAccessor Accessor { get; set; } = null!;
9-
public InternalConverter? Converter { get; set; }
9+
public InternalConverter Converter { get; set; } = null!;
1010
public bool IsRepeated { get; set; }
1111
public Type FieldType { get; set; } = null!;
1212
public string JsonName { get; set; } = null!;
1313
public bool IsOneOf { get; set; }
1414
public bool IsMap { get; set; }
15+
public EnumDescriptor? EnumType { get; set; }
1516
}

src/Protobuf.System.Text.Json/InternalConverters/InternalConverterFactory.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
using System.Text.Json;
2+
13
namespace Protobuf.System.Text.Json.InternalConverters;
24

35
internal class InternalConverterFactory
46
{
5-
public static InternalConverter Create(FieldInfo fieldInfo)
7+
public static InternalConverter Create(FieldInfo fieldInfo, JsonSerializerOptions jsonSerializerOptions)
68
{
79
if (fieldInfo.IsMap)
810
{
@@ -16,6 +18,11 @@ public static InternalConverter Create(FieldInfo fieldInfo)
1618
var internalConverter = (InternalConverter) Activator.CreateInstance(converterType)!;
1719
return internalConverter;
1820
}
21+
else if (fieldInfo.EnumType != null)
22+
{
23+
var internalConverter = (InternalConverter) Activator.CreateInstance(typeof(ProtoEnumConverter), args: new object[] { fieldInfo.EnumType, jsonSerializerOptions.Encoder! })!;
24+
return internalConverter;
25+
}
1926
else
2027
{
2128
var converterType = typeof(FieldConverter<>).MakeGenericType(fieldInfo.FieldType);
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using System.Text.Encodings.Web;
2+
using System.Text.Json;
3+
using Google.Protobuf;
4+
using Google.Protobuf.Reflection;
5+
6+
namespace Protobuf.System.Text.Json.InternalConverters;
7+
8+
internal class ProtoEnumConverter : InternalConverter
9+
{
10+
private readonly Dictionary<string, int> _lookup;
11+
private readonly Dictionary<int, JsonEncodedText> _reversedLookup;
12+
private readonly Type _clrType;
13+
14+
public ProtoEnumConverter(EnumDescriptor fieldInfoEnumType, JavaScriptEncoder? encoder)
15+
{
16+
_clrType = fieldInfoEnumType.ClrType;
17+
_lookup = fieldInfoEnumType.Values.ToDictionary(x => x.Name, x => x.Number);
18+
_reversedLookup = fieldInfoEnumType.Values.ToDictionary(x => x.Number, x => JsonEncodedText.Encode(x.Name, encoder));
19+
}
20+
21+
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
22+
{
23+
var intValue = (int) value;
24+
if (_reversedLookup.TryGetValue(intValue, out var stringValue))
25+
{
26+
writer.WriteStringValue(stringValue);
27+
}
28+
else
29+
{
30+
writer.WriteNumberValue(intValue);
31+
}
32+
}
33+
34+
public override void Read(ref Utf8JsonReader reader, IMessage obj, Type typeToConvert, JsonSerializerOptions options, IFieldAccessor fieldAccessor)
35+
{
36+
if (reader.TokenType == JsonTokenType.String)
37+
{
38+
if (reader.GetString() is { } stringValue)
39+
{
40+
if (_lookup.TryGetValue(stringValue, out var value))
41+
{
42+
fieldAccessor.SetValue(obj, value);
43+
return;
44+
}
45+
46+
throw new JsonException($"'{stringValue}' is not a valid value for type {_clrType.FullName}.");
47+
}
48+
}
49+
else if (reader.TokenType == JsonTokenType.Number)
50+
{
51+
if (reader.TryGetInt32(out var value))
52+
{
53+
fieldAccessor.SetValue(obj, value);
54+
}
55+
}
56+
}
57+
}

src/Protobuf.System.Text.Json/JsonProtobufSerializerOptions.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
namespace Protobuf.System.Text.Json;
22

3+
/// <summary>
4+
/// Provides a set of options to configure the behavior of the JSON-Protobuf serialization and deserialization process.
5+
/// </summary>
36
public class JsonProtobufSerializerOptions
47
{
58
/// <summary>
@@ -20,7 +23,7 @@ public class JsonProtobufSerializerOptions
2023
/// </summary>
2124
public bool TreatDurationAsTimeSpan { get; set; } = true;
2225

23-
26+
2427
/// <summary>
2528
/// Controls how <see cref="Google.Protobuf.WellKnownTypes.Timestamp"/> fields are handled.
2629
/// When set to true, <see cref="Google.Protobuf.WellKnownTypes.Timestamp"/> properties will
@@ -29,4 +32,13 @@ public class JsonProtobufSerializerOptions
2932
/// The default value is true.
3033
/// </summary>
3134
public bool TreatTimestampAsDateTime { get; set; } = true;
35+
36+
/// <summary>
37+
/// Controls how enums defined as part of the protobuf contract are handled.
38+
/// When set to true enum values will be serialized as strings based on the naming
39+
/// specified in the .proto file. The same format will be expected during deserialization.
40+
/// The default value is false.
41+
/// </summary>
42+
///
43+
public bool UseStringProtoEnumValueNames { get; set; }
3244
}

src/Protobuf.System.Text.Json/Protobuf.System.Text.Json.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,19 @@
2222
</PropertyGroup>
2323

2424
<ItemGroup>
25-
<PackageReference Include="Google.Protobuf" Version="3.19.0"/>
25+
<PackageReference Include="Google.Protobuf" Version="3.19.0" />
2626
</ItemGroup>
2727

2828
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
29-
<PackageReference Include="System.Text.Json" Version="5.0.0"/>
29+
<PackageReference Include="System.Text.Json" Version="5.0.0" />
3030
</ItemGroup>
3131

3232
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.1' ">
33-
<PackageReference Include="System.Text.Json" Version="5.0.0"/>
33+
<PackageReference Include="System.Text.Json" Version="5.0.0" />
3434
</ItemGroup>
3535

3636
<ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp3.1' ">
37-
<PackageReference Include="System.Text.Json" Version="5.0.0"/>
37+
<PackageReference Include="System.Text.Json" Version="5.0.0" />
3838
</ItemGroup>
3939

4040
</Project>

src/Protobuf.System.Text.Json/ProtobufConverter.cs

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ namespace Protobuf.System.Text.Json;
1717
public ProtobufConverter(JsonSerializerOptions jsonSerializerOptions, JsonProtobufSerializerOptions jsonProtobufSerializerOptions)
1818
{
1919
_defaultIgnoreCondition = jsonSerializerOptions.DefaultIgnoreCondition;
20-
20+
2121
var type = typeof(T);
2222

2323
var propertyTypeLookup = type.GetProperties().ToDictionary(x => x.Name, x => x.PropertyType);
@@ -27,14 +27,20 @@ public ProtobufConverter(JsonSerializerOptions jsonSerializerOptions, JsonProtob
2727

2828
var convertNameFunc = GetConvertNameFunc(jsonSerializerOptions.PropertyNamingPolicy, jsonProtobufSerializerOptions.UseProtobufJsonNames);
2929

30-
_fields = messageDescriptor.Fields.InDeclarationOrder().Select(fieldDescriptor => new FieldInfo
30+
_fields = messageDescriptor.Fields.InDeclarationOrder().Select(fieldDescriptor =>
3131
{
32-
Accessor = fieldDescriptor.Accessor,
33-
IsRepeated = fieldDescriptor.IsRepeated,
34-
IsMap = fieldDescriptor.IsMap,
35-
FieldType = FieldTypeResolver.ResolverFieldType(fieldDescriptor, propertyTypeLookup),
36-
JsonName = convertNameFunc(fieldDescriptor),
37-
IsOneOf = fieldDescriptor.ContainingOneof != null
32+
var fieldInfo = new FieldInfo
33+
{
34+
Accessor = fieldDescriptor.Accessor,
35+
IsRepeated = fieldDescriptor.IsRepeated,
36+
EnumType = jsonProtobufSerializerOptions.UseStringProtoEnumValueNames ? fieldDescriptor.EnumType : null,
37+
IsMap = fieldDescriptor.IsMap,
38+
FieldType = FieldTypeResolver.ResolverFieldType(fieldDescriptor, propertyTypeLookup),
39+
JsonName = convertNameFunc(fieldDescriptor),
40+
IsOneOf = fieldDescriptor.ContainingOneof != null,
41+
};
42+
fieldInfo.Converter = InternalConverterFactory.Create(fieldInfo, jsonSerializerOptions);
43+
return fieldInfo;
3844
}).ToArray();
3945

4046
var stringComparer = jsonSerializerOptions.PropertyNameCaseInsensitive ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
@@ -98,7 +104,6 @@ private static Func<FieldDescriptor, string> GetConvertNameFunc(JsonNamingPolicy
98104
}
99105

100106
reader.Read();
101-
fieldInfo.Converter ??= InternalConverterFactory.Create(fieldInfo);
102107
fieldInfo.Converter.Read(ref reader, obj, fieldInfo.FieldType, options, fieldInfo.Accessor);
103108
}
104109

@@ -128,7 +133,6 @@ public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOption
128133
if (_defaultIgnoreCondition is JsonIgnoreCondition.Never or not JsonIgnoreCondition.WhenWritingDefault)
129134
{
130135
writer.WritePropertyName(fieldInfo.JsonName);
131-
fieldInfo.Converter ??= InternalConverterFactory.Create(fieldInfo);
132136
fieldInfo.Converter.Write(writer, propertyValue, options);
133137
}
134138
}

test/Protobuf.System.Text.Json.Tests/ContractEvolutionTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public void Should_deserialize_the_old_version_of_a_message_using_the_new_versio
3838
DoubleProperty = 1d,
3939
};
4040
var jsonSerializerOptions = TestHelper.CreateJsonSerializerOptions();
41-
41+
4242
// Act
4343
var payload = JsonSerializer.Serialize(msg, jsonSerializerOptions);
4444
var deserialized = JsonSerializer.Deserialize<MessageWithVersionMismatchV2>(payload, jsonSerializerOptions);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"enumField": 99
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"enumField": "FIRST_OPTION"
3+
}

0 commit comments

Comments
 (0)