Skip to content

Commit 083a12c

Browse files
committed
Add maps support
1 parent 438f1b3 commit 083a12c

7 files changed

Lines changed: 148 additions & 12 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ internal class FieldInfo
1111
public Type FieldType { get; init; }
1212
public string JsonName { get; set; }
1313
public bool IsOneOf { get; set; }
14+
public bool IsMap { get; set; }
1415
}

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,23 @@ namespace System.Text.Json.Protobuf.InternalConverters;
22

33
internal class InternalConverterFactory
44
{
5-
public static InternalConverter Create(Type fieldType, bool isRepeated)
5+
public static InternalConverter Create(FieldInfo fieldInfo)
66
{
7-
if (isRepeated)
7+
if (fieldInfo.IsMap)
88
{
9-
var converterType = typeof(RepeatedFieldConverter<>).MakeGenericType(fieldType);
9+
var converterType = typeof(MapConverter<,>).MakeGenericType(fieldInfo.FieldType.GenericTypeArguments);
10+
var internalConverter = (InternalConverter) Activator.CreateInstance(converterType)!;
11+
return internalConverter;
12+
}
13+
else if (fieldInfo.IsRepeated)
14+
{
15+
var converterType = typeof(RepeatedFieldConverter<>).MakeGenericType(fieldInfo.FieldType);
1016
var internalConverter = (InternalConverter) Activator.CreateInstance(converterType)!;
1117
return internalConverter;
1218
}
1319
else
1420
{
15-
var converterType = typeof(FieldConverter<>).MakeGenericType(fieldType);
21+
var converterType = typeof(FieldConverter<>).MakeGenericType(fieldInfo.FieldType);
1622
var internalConverter = (InternalConverter) Activator.CreateInstance(converterType)!;
1723
return internalConverter;
1824
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using System.Text.Json.Serialization;
2+
using Google.Protobuf;
3+
using Google.Protobuf.Collections;
4+
using Google.Protobuf.Reflection;
5+
6+
namespace System.Text.Json.Protobuf.InternalConverters;
7+
8+
internal class MapConverter<TKey, TValue> : InternalConverter
9+
{
10+
private JsonConverter<IDictionary<TKey, TValue>>? _jsonConverter;
11+
12+
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
13+
{
14+
_jsonConverter ??= GetConverter(options);
15+
_jsonConverter.Write(writer, (IDictionary<TKey, TValue>) value, options);
16+
}
17+
18+
public override void Read(ref Utf8JsonReader reader, IMessage obj, Type typeToConvert, JsonSerializerOptions options,
19+
IFieldAccessor fieldAccessor)
20+
{
21+
_jsonConverter ??= GetConverter(options);
22+
23+
var elements = _jsonConverter.Read(ref reader, typeToConvert, options);
24+
if (elements == null)
25+
{
26+
return;
27+
}
28+
29+
var value = (MapField<TKey,TValue>)fieldAccessor.GetValue(obj);
30+
value.Add(elements);
31+
}
32+
33+
private static JsonConverter<IDictionary<TKey, TValue>> GetConverter(JsonSerializerOptions options)
34+
{
35+
var dictionaryType = typeof(IDictionary<,>).MakeGenericType(typeof(TKey), typeof(TValue));
36+
return (JsonConverter<IDictionary<TKey, TValue>>) options.GetConverter(dictionaryType);
37+
}
38+
}

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

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ namespace System.Text.Json.Protobuf;
1313

1414
public ProtobufConverter(JsonNamingPolicy? namingPolicy, JsonProtobufSerializerOptions options)
1515
{
16-
var propertyInfo = typeof(T).GetProperty("Descriptor", BindingFlags.Public | BindingFlags.Static);
16+
var type = typeof(T);
17+
18+
var propertyTypeLookup = type.GetProperties().ToDictionary(x => x.Name, x => x.PropertyType);
19+
20+
var propertyInfo = type.GetProperty("Descriptor", BindingFlags.Public | BindingFlags.Static);
1721
var messageDescriptor = (MessageDescriptor) propertyInfo?.GetValue(null, null)!;
1822

1923
var convertNameFunc = GetConvertNameFunc(namingPolicy, options.UseProtobufJsonNames);
@@ -22,7 +26,8 @@ public ProtobufConverter(JsonNamingPolicy? namingPolicy, JsonProtobufSerializerO
2226
{
2327
Accessor = fieldDescriptor.Accessor,
2428
IsRepeated = fieldDescriptor.IsRepeated,
25-
FieldType = GetFieldType(fieldDescriptor),
29+
IsMap = fieldDescriptor.IsMap,
30+
FieldType = GetFieldType(fieldDescriptor, propertyTypeLookup),
2631
JsonName = convertNameFunc(fieldDescriptor),
2732
IsOneOf = fieldDescriptor.ContainingOneof != null
2833
}).ToArray();
@@ -72,7 +77,7 @@ public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerial
7277
continue;
7378
}
7479

75-
fieldInfo.Converter ??= InternalConverterFactory.Create(fieldInfo.FieldType, fieldInfo.IsRepeated);
80+
fieldInfo.Converter ??= InternalConverterFactory.Create(fieldInfo);
7681

7782
reader.Read();
7883
fieldInfo.Converter.Read(ref reader, obj, fieldInfo.FieldType, options, fieldInfo.Accessor);
@@ -102,17 +107,17 @@ public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOption
102107
if (obj is { } propertyValue)
103108
{
104109
writer.WritePropertyName(fieldInfo.JsonName);
105-
fieldInfo.Converter ??= InternalConverterFactory.Create(fieldInfo.FieldType, fieldInfo.IsRepeated);
110+
fieldInfo.Converter ??= InternalConverterFactory.Create(fieldInfo);
106111
fieldInfo.Converter.Write(writer, propertyValue, options);
107112
}
108113
}
109114

110115
writer.WriteEndObject();
111116
}
112117

113-
private Type GetFieldType(FieldDescriptor fieldType)
118+
private Type GetFieldType(FieldDescriptor fieldDescriptor, Dictionary<string, Type> propertyTypeLookup)
114119
{
115-
switch (fieldType.FieldType)
120+
switch (fieldDescriptor.FieldType)
116121
{
117122
case FieldType.Double:
118123
return typeof(double);
@@ -136,10 +141,12 @@ private Type GetFieldType(FieldDescriptor fieldType)
136141
return typeof(bool);
137142
case FieldType.String:
138143
return typeof(string);
144+
case FieldType.Message when fieldDescriptor.MessageType.ClrType != null:
145+
return fieldDescriptor.MessageType.ClrType;
139146
case FieldType.Message:
140-
return fieldType.MessageType.ClrType;
147+
return propertyTypeLookup[fieldDescriptor.PropertyName];
141148
default:
142-
throw new ArgumentOutOfRangeException(nameof(fieldType), $"FieldType: '{fieldType}' is not supported.");
149+
throw new ArgumentOutOfRangeException(nameof(fieldDescriptor), $"FieldType: '{fieldDescriptor}' is not supported.");
143150
}
144151
}
145152
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"mapIntToString": {
3+
"1": "1-value"
4+
},
5+
"mapStringToComplexType": {
6+
"string_key": {
7+
"mapStringToInt": {
8+
"a": 1
9+
}
10+
}
11+
}
12+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using System.Text.Json.Protobuf.Tests.Utils;
2+
using Shouldly;
3+
using SmartAnalyzers.ApprovalTestsExtensions;
4+
using Xunit;
5+
6+
namespace System.Text.Json.Protobuf.Tests;
7+
8+
public class MessageWithMapsTests
9+
{
10+
[Fact]
11+
public void Should_serialize_message_with_map_field()
12+
{
13+
// Arrange
14+
var msg = new MessageWithMaps
15+
{
16+
MapIntToString = {[1] = "1-value"},
17+
MapStringToComplexType =
18+
{
19+
["string_key"] = new NestedMessageAsKey
20+
{
21+
MapStringToInt = {["a"] = 1}
22+
}
23+
}
24+
};
25+
var jsonSerializerOptions = TestHelper.CreateJsonSerializerOptions();
26+
27+
// Act
28+
var serialized = JsonSerializer.Serialize(msg, jsonSerializerOptions);
29+
30+
// Assert
31+
var approver = new ExplicitApprover();
32+
approver.VerifyJson(serialized);
33+
}
34+
35+
[Fact]
36+
public void Should_deserialize_message_with_map_field()
37+
{
38+
// Arrange
39+
var msg = new MessageWithMaps
40+
{
41+
MapIntToString = {[1] = "1-value"},
42+
MapStringToComplexType =
43+
{
44+
["string_key"] = new NestedMessageAsKey
45+
{
46+
MapStringToInt = {["a"] = 1}
47+
}
48+
}
49+
};
50+
var jsonSerializerOptions = TestHelper.CreateJsonSerializerOptions();
51+
52+
// Act
53+
var serialized = JsonSerializer.Serialize(msg, jsonSerializerOptions);
54+
var deserialized = JsonSerializer.Deserialize<MessageWithMaps>(serialized, jsonSerializerOptions);
55+
56+
// Assert
57+
deserialized.ShouldNotBeNull();
58+
deserialized.ShouldBeEquivalentTo(msg);
59+
}
60+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
syntax = "proto3";
2+
3+
option csharp_namespace = "System.Text.Json.Protobuf.Tests";
4+
5+
message MessageWithMaps {
6+
map<int32, string> map_int_to_string = 1;
7+
map<string, NestedMessageAsKey> map_string_to_complex_type = 2;
8+
}
9+
10+
message NestedMessageAsKey {
11+
map<string, int32> map_string_to_int = 1;
12+
}

0 commit comments

Comments
 (0)