Skip to content

Commit 3bc8b8a

Browse files
authored
Add PropertyNamingSource enum to support proto field names (#70)
1 parent c248b75 commit 3bc8b8a

11 files changed

Lines changed: 287 additions & 13 deletions

README.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,26 @@ var deserialized = JsonSerializer.Deserialize<SimpleMessage>(payload, jsonSerial
4444
## Configuration
4545
The library offers several configuration options to fine-tune protobuf serialization. You can modify the default settings using a delegate passed to the AddProtobufSupport method. The available options are described below:
4646

47-
### UseProtobufJsonNames
47+
### PropertyNamingSource
48+
This option specifies the source for property names in JSON serialization. The default value is `PropertyNamingSource.Default`.
49+
50+
Available values:
51+
- `PropertyNamingSource.Default`: Use the default `PropertyNamingPolicy` from `JsonSerializerOptions`.
52+
- `PropertyNamingSource.ProtobufJsonName`: Use the JsonName from the protobuf contract. This is usually the lower-camel-cased form of the field name, but can be overridden using the `json_name` option in the .proto file.
53+
- `PropertyNamingSource.ProtobufFieldName`: Use the original field name as defined in the .proto file (e.g., "double_property" instead of "doubleProperty").
54+
55+
Example:
56+
```csharp
57+
var jsonSerializerOptions = new JsonSerializerOptions();
58+
jsonSerializerOptions.AddProtobufSupport(options =>
59+
{
60+
options.PropertyNamingSource = PropertyNamingSource.ProtobufFieldName;
61+
});
62+
```
63+
64+
### UseProtobufJsonNames (Obsolete)
65+
**Note:** This property is obsolete and will be removed in a future version. Use `PropertyNamingSource` instead.
66+
4867
This option defines how property names should be resolved for protobuf contracts. When set to `true`, the `PropertyNamingPolicy` will be ignored, and property names will be derived from the protobuf contract. The default value is `false`.
4968

5069
### TreatDurationAsTimeSpan

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,18 @@ public class JsonProtobufSerializerOptions
1212
/// option in the .proto file.
1313
/// The default value is false.
1414
/// </summary>
15-
public bool UseProtobufJsonNames { get; set; }
15+
[Obsolete("Use PropertyNamingSource instead. This property will be removed in a future version.")]
16+
public bool UseProtobufJsonNames
17+
{
18+
get => PropertyNamingSource == PropertyNamingSource.ProtobufJsonName;
19+
set => PropertyNamingSource = value ? PropertyNamingSource.ProtobufJsonName : PropertyNamingSource.Default;
20+
}
21+
22+
/// <summary>
23+
/// Specifies the source for property names in JSON serialization.
24+
/// The default value is <see cref="PropertyNamingSource.Default"/>.
25+
/// </summary>
26+
public PropertyNamingSource PropertyNamingSource { get; set; } = PropertyNamingSource.Default;
1627

1728
/// <summary>
1829
/// Controls how <see cref="Google.Protobuf.WellKnownTypes.Duration"/> fields are handled.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
namespace Protobuf.System.Text.Json;
2+
3+
/// <summary>
4+
/// Specifies the source for property names in JSON serialization.
5+
/// </summary>
6+
public enum PropertyNamingSource
7+
{
8+
/// <summary>
9+
/// Use the default PropertyNamingPolicy from JsonSerializerOptions.
10+
/// </summary>
11+
Default = 0,
12+
13+
/// <summary>
14+
/// Use the JsonName from the protobuf contract.
15+
/// This is usually the lower-camel-cased form of the field name,
16+
/// but can be overridden using the json_name option in the .proto file.
17+
/// </summary>
18+
ProtobufJsonName = 1,
19+
20+
/// <summary>
21+
/// Use the original field name as defined in the .proto file.
22+
/// For example, "double_property" instead of "doubleProperty".
23+
/// </summary>
24+
ProtobufFieldName = 2
25+
}

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

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public ProtobufConverter(JsonSerializerOptions jsonSerializerOptions, JsonProtob
2424
var propertyInfo = type.GetProperty("Descriptor", BindingFlags.Public | BindingFlags.Static);
2525
var messageDescriptor = (MessageDescriptor) propertyInfo?.GetValue(null, null)!;
2626

27-
var convertNameFunc = GetConvertNameFunc(jsonSerializerOptions.PropertyNamingPolicy, jsonProtobufSerializerOptions.UseProtobufJsonNames);
27+
var convertNameFunc = GetConvertNameFunc(jsonSerializerOptions.PropertyNamingPolicy, jsonProtobufSerializerOptions.PropertyNamingSource);
2828

2929
_fields = messageDescriptor.Fields.InDeclarationOrder().Select(fieldDescriptor =>
3030
{
@@ -49,19 +49,24 @@ public ProtobufConverter(JsonSerializerOptions jsonSerializerOptions, JsonProtob
4949
_fieldsLookup = _fields.ToDictionary(x => x.JsonName, x => x, stringComparer);
5050
}
5151

52-
private static Func<FieldDescriptor, string> GetConvertNameFunc(JsonNamingPolicy? jsonNamingPolicy, bool useProtobufJsonNames)
52+
private static Func<FieldDescriptor, string> GetConvertNameFunc(JsonNamingPolicy? jsonNamingPolicy, PropertyNamingSource propertyNamingSource)
5353
{
54-
if (useProtobufJsonNames)
54+
switch (propertyNamingSource)
5555
{
56-
return descriptor => descriptor.JsonName;
57-
}
58-
59-
if (jsonNamingPolicy != null)
60-
{
61-
return descriptor => jsonNamingPolicy.ConvertName(descriptor.PropertyName);
56+
case PropertyNamingSource.ProtobufJsonName:
57+
return descriptor => descriptor.JsonName;
58+
59+
case PropertyNamingSource.ProtobufFieldName:
60+
return descriptor => descriptor.Name;
61+
62+
case PropertyNamingSource.Default:
63+
default:
64+
if (jsonNamingPolicy != null)
65+
{
66+
return descriptor => jsonNamingPolicy.ConvertName(descriptor.PropertyName);
67+
}
68+
return descriptor => descriptor.PropertyName;
6269
}
63-
64-
return descriptor => descriptor.PropertyName;
6570
}
6671

6772
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"double_property": 2.5,
3+
"float_property": 0,
4+
"int_32_property": 0,
5+
"int_64_property": 0,
6+
"uint_32_property": 0,
7+
"uint_64_property": 0,
8+
"sint_32_property": 0,
9+
"sint_64_property": 0,
10+
"fixed_32_property": 0,
11+
"fixed_64_property": 0,
12+
"sfixed_32_property": 0,
13+
"sfixed_64_property": 0,
14+
"bool_property": false,
15+
"string_property": "",
16+
"bytes_property": ""
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"doubleProperty": 2.5,
3+
"floatProperty": 0,
4+
"int32Property": 0,
5+
"int64Property": 0,
6+
"uint32Property": 0,
7+
"uint64Property": 0,
8+
"sint32Property": 0,
9+
"sint64Property": 0,
10+
"fixed32Property": 0,
11+
"fixed64Property": 0,
12+
"sfixed32Property": 0,
13+
"sfixed64Property": 0,
14+
"boolProperty": false,
15+
"stringProperty": "",
16+
"bytesProperty": ""
17+
}

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

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ public void Should_ignore_PropertyNamingPolicy_when_UseProtobufJsonNames_set_to_
3737
};
3838
var jsonSerializerOptions = new JsonSerializerOptions();
3939
jsonSerializerOptions.PropertyNamingPolicy = new JsonLowerCaseNamingPolicy();
40+
#pragma warning disable CS0618 // Type or member is obsolete
4041
jsonSerializerOptions.AddProtobufSupport(options => options.UseProtobufJsonNames = true);
42+
#pragma warning restore CS0618 // Type or member is obsolete
4143

4244
// Act
4345
var serialized = JsonSerializer.Serialize(msg, jsonSerializerOptions);
@@ -47,6 +49,62 @@ public void Should_ignore_PropertyNamingPolicy_when_UseProtobufJsonNames_set_to_
4749
approver.VerifyJson(serialized);
4850
}
4951

52+
[Fact]
53+
public void Should_use_protobuf_json_name_when_PropertyNamingSource_set_to_ProtobufJsonName()
54+
{
55+
// Arrange
56+
var msg = new SimpleMessage
57+
{
58+
DoubleProperty = 2.5d
59+
};
60+
var jsonSerializerOptions = new JsonSerializerOptions();
61+
jsonSerializerOptions.PropertyNamingPolicy = new JsonLowerCaseNamingPolicy();
62+
jsonSerializerOptions.AddProtobufSupport(options => options.PropertyNamingSource = PropertyNamingSource.ProtobufJsonName);
63+
64+
// Act
65+
var serialized = JsonSerializer.Serialize(msg, jsonSerializerOptions);
66+
67+
// Assert
68+
var approver = new ExplicitApprover();
69+
approver.VerifyJson(serialized);
70+
}
71+
72+
[Fact]
73+
public void Should_use_protobuf_field_name_when_PropertyNamingSource_set_to_ProtobufFieldName()
74+
{
75+
// Arrange
76+
var msg = new SimpleMessage
77+
{
78+
DoubleProperty = 2.5d
79+
};
80+
var jsonSerializerOptions = new JsonSerializerOptions();
81+
jsonSerializerOptions.PropertyNamingPolicy = new JsonLowerCaseNamingPolicy();
82+
jsonSerializerOptions.AddProtobufSupport(options => options.PropertyNamingSource = PropertyNamingSource.ProtobufFieldName);
83+
84+
// Act
85+
var serialized = JsonSerializer.Serialize(msg, jsonSerializerOptions);
86+
87+
// Assert
88+
var approver = new ExplicitApprover();
89+
approver.VerifyJson(serialized);
90+
}
91+
92+
[Fact]
93+
public void Should_deserialize_using_protobuf_field_name_when_PropertyNamingSource_set_to_ProtobufFieldName()
94+
{
95+
// Arrange
96+
var json = "{\"double_property\": 2.5}";
97+
var jsonSerializerOptions = new JsonSerializerOptions();
98+
jsonSerializerOptions.AddProtobufSupport(options => options.PropertyNamingSource = PropertyNamingSource.ProtobufFieldName);
99+
100+
// Act
101+
var deserialized = JsonSerializer.Deserialize<SimpleMessage>(json, jsonSerializerOptions);
102+
103+
// Assert
104+
Assert.NotNull(deserialized);
105+
Assert.Equal(2.5d, deserialized.DoubleProperty);
106+
}
107+
50108
private class JsonLowerCaseNamingPolicy : JsonNamingPolicy
51109
{
52110
public override string ConvertName(string name)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"customDoubleProperty": 2.5,
3+
"stringProperty": "test"
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"double_property": 2.5,
3+
"string_property": "test"
4+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Protobuf.Tests;
3+
using SmartAnalyzers.ApprovalTestsExtensions;
4+
using Xunit;
5+
6+
namespace Protobuf.System.Text.Json.Tests;
7+
8+
public class PropertyNamingSourceTests
9+
{
10+
[Fact]
11+
public void Should_use_custom_json_name_when_PropertyNamingSource_is_ProtobufJsonName()
12+
{
13+
// Arrange
14+
var msg = new MessageWithCustomJsonName
15+
{
16+
DoubleProperty = 2.5d,
17+
StringProperty = "test"
18+
};
19+
var jsonSerializerOptions = new JsonSerializerOptions();
20+
jsonSerializerOptions.AddProtobufSupport(options => options.PropertyNamingSource = PropertyNamingSource.ProtobufJsonName);
21+
22+
// Act
23+
var serialized = JsonSerializer.Serialize(msg, jsonSerializerOptions);
24+
25+
// Assert
26+
var approver = new ExplicitApprover();
27+
approver.VerifyJson(serialized);
28+
}
29+
30+
[Fact]
31+
public void Should_use_proto_field_name_when_PropertyNamingSource_is_ProtobufFieldName()
32+
{
33+
// Arrange
34+
var msg = new MessageWithCustomJsonName
35+
{
36+
DoubleProperty = 2.5d,
37+
StringProperty = "test"
38+
};
39+
var jsonSerializerOptions = new JsonSerializerOptions();
40+
jsonSerializerOptions.AddProtobufSupport(options => options.PropertyNamingSource = PropertyNamingSource.ProtobufFieldName);
41+
42+
// Act
43+
var serialized = JsonSerializer.Serialize(msg, jsonSerializerOptions);
44+
45+
// Assert
46+
var approver = new ExplicitApprover();
47+
approver.VerifyJson(serialized);
48+
}
49+
50+
[Fact]
51+
public void Should_deserialize_using_custom_json_name_when_PropertyNamingSource_is_ProtobufJsonName()
52+
{
53+
// Arrange
54+
var json = "{\"customDoubleProperty\": 2.5, \"stringProperty\": \"test\"}";
55+
var jsonSerializerOptions = new JsonSerializerOptions();
56+
jsonSerializerOptions.AddProtobufSupport(options => options.PropertyNamingSource = PropertyNamingSource.ProtobufJsonName);
57+
58+
// Act
59+
var deserialized = JsonSerializer.Deserialize<MessageWithCustomJsonName>(json, jsonSerializerOptions);
60+
61+
// Assert
62+
Assert.NotNull(deserialized);
63+
Assert.Equal(2.5d, deserialized.DoubleProperty);
64+
Assert.Equal("test", deserialized.StringProperty);
65+
}
66+
67+
[Fact]
68+
public void Should_deserialize_using_proto_field_name_when_PropertyNamingSource_is_ProtobufFieldName()
69+
{
70+
// Arrange
71+
var json = "{\"double_property\": 2.5, \"string_property\": \"test\"}";
72+
var jsonSerializerOptions = new JsonSerializerOptions();
73+
jsonSerializerOptions.AddProtobufSupport(options => options.PropertyNamingSource = PropertyNamingSource.ProtobufFieldName);
74+
75+
// Act
76+
var deserialized = JsonSerializer.Deserialize<MessageWithCustomJsonName>(json, jsonSerializerOptions);
77+
78+
// Assert
79+
Assert.NotNull(deserialized);
80+
Assert.Equal(2.5d, deserialized.DoubleProperty);
81+
Assert.Equal("test", deserialized.StringProperty);
82+
}
83+
84+
[Fact]
85+
public void Should_round_trip_with_ProtobufFieldName()
86+
{
87+
// Arrange
88+
var original = new MessageWithCustomJsonName
89+
{
90+
DoubleProperty = 2.5d,
91+
StringProperty = "test"
92+
};
93+
var jsonSerializerOptions = new JsonSerializerOptions();
94+
jsonSerializerOptions.AddProtobufSupport(options => options.PropertyNamingSource = PropertyNamingSource.ProtobufFieldName);
95+
96+
// Act
97+
var serialized = JsonSerializer.Serialize(original, jsonSerializerOptions);
98+
var deserialized = JsonSerializer.Deserialize<MessageWithCustomJsonName>(serialized, jsonSerializerOptions);
99+
100+
// Assert
101+
Assert.NotNull(deserialized);
102+
Assert.Equal(original.DoubleProperty, deserialized.DoubleProperty);
103+
Assert.Equal(original.StringProperty, deserialized.StringProperty);
104+
}
105+
}

0 commit comments

Comments
 (0)