diff --git a/README.md b/README.md index 380525a..d48c0f8 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,26 @@ var deserialized = JsonSerializer.Deserialize(payload, jsonSerial ## Configuration 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: -### UseProtobufJsonNames +### PropertyNamingSource +This option specifies the source for property names in JSON serialization. The default value is `PropertyNamingSource.Default`. + +Available values: +- `PropertyNamingSource.Default`: Use the default `PropertyNamingPolicy` from `JsonSerializerOptions`. +- `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. +- `PropertyNamingSource.ProtobufFieldName`: Use the original field name as defined in the .proto file (e.g., "double_property" instead of "doubleProperty"). + +Example: +```csharp +var jsonSerializerOptions = new JsonSerializerOptions(); +jsonSerializerOptions.AddProtobufSupport(options => +{ + options.PropertyNamingSource = PropertyNamingSource.ProtobufFieldName; +}); +``` + +### UseProtobufJsonNames (Obsolete) +**Note:** This property is obsolete and will be removed in a future version. Use `PropertyNamingSource` instead. + 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`. ### TreatDurationAsTimeSpan diff --git a/src/Protobuf.System.Text.Json/JsonProtobufSerializerOptions.cs b/src/Protobuf.System.Text.Json/JsonProtobufSerializerOptions.cs index 46095e7..3157367 100644 --- a/src/Protobuf.System.Text.Json/JsonProtobufSerializerOptions.cs +++ b/src/Protobuf.System.Text.Json/JsonProtobufSerializerOptions.cs @@ -12,7 +12,18 @@ public class JsonProtobufSerializerOptions /// option in the .proto file. /// The default value is false. /// - public bool UseProtobufJsonNames { get; set; } + [Obsolete("Use PropertyNamingSource instead. This property will be removed in a future version.")] + public bool UseProtobufJsonNames + { + get => PropertyNamingSource == PropertyNamingSource.ProtobufJsonName; + set => PropertyNamingSource = value ? PropertyNamingSource.ProtobufJsonName : PropertyNamingSource.Default; + } + + /// + /// Specifies the source for property names in JSON serialization. + /// The default value is . + /// + public PropertyNamingSource PropertyNamingSource { get; set; } = PropertyNamingSource.Default; /// /// Controls how fields are handled. diff --git a/src/Protobuf.System.Text.Json/PropertyNamingSource.cs b/src/Protobuf.System.Text.Json/PropertyNamingSource.cs new file mode 100644 index 0000000..8b14e76 --- /dev/null +++ b/src/Protobuf.System.Text.Json/PropertyNamingSource.cs @@ -0,0 +1,25 @@ +namespace Protobuf.System.Text.Json; + +/// +/// Specifies the source for property names in JSON serialization. +/// +public enum PropertyNamingSource +{ + /// + /// Use the default PropertyNamingPolicy from JsonSerializerOptions. + /// + Default = 0, + + /// + /// 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. + /// + ProtobufJsonName = 1, + + /// + /// Use the original field name as defined in the .proto file. + /// For example, "double_property" instead of "doubleProperty". + /// + ProtobufFieldName = 2 +} diff --git a/src/Protobuf.System.Text.Json/ProtobufConverter.cs b/src/Protobuf.System.Text.Json/ProtobufConverter.cs index 8e0986a..569a7ab 100644 --- a/src/Protobuf.System.Text.Json/ProtobufConverter.cs +++ b/src/Protobuf.System.Text.Json/ProtobufConverter.cs @@ -24,7 +24,7 @@ public ProtobufConverter(JsonSerializerOptions jsonSerializerOptions, JsonProtob var propertyInfo = type.GetProperty("Descriptor", BindingFlags.Public | BindingFlags.Static); var messageDescriptor = (MessageDescriptor) propertyInfo?.GetValue(null, null)!; - var convertNameFunc = GetConvertNameFunc(jsonSerializerOptions.PropertyNamingPolicy, jsonProtobufSerializerOptions.UseProtobufJsonNames); + var convertNameFunc = GetConvertNameFunc(jsonSerializerOptions.PropertyNamingPolicy, jsonProtobufSerializerOptions.PropertyNamingSource); _fields = messageDescriptor.Fields.InDeclarationOrder().Select(fieldDescriptor => { @@ -49,19 +49,24 @@ public ProtobufConverter(JsonSerializerOptions jsonSerializerOptions, JsonProtob _fieldsLookup = _fields.ToDictionary(x => x.JsonName, x => x, stringComparer); } - private static Func GetConvertNameFunc(JsonNamingPolicy? jsonNamingPolicy, bool useProtobufJsonNames) + private static Func GetConvertNameFunc(JsonNamingPolicy? jsonNamingPolicy, PropertyNamingSource propertyNamingSource) { - if (useProtobufJsonNames) + switch (propertyNamingSource) { - return descriptor => descriptor.JsonName; - } - - if (jsonNamingPolicy != null) - { - return descriptor => jsonNamingPolicy.ConvertName(descriptor.PropertyName); + case PropertyNamingSource.ProtobufJsonName: + return descriptor => descriptor.JsonName; + + case PropertyNamingSource.ProtobufFieldName: + return descriptor => descriptor.Name; + + case PropertyNamingSource.Default: + default: + if (jsonNamingPolicy != null) + { + return descriptor => jsonNamingPolicy.ConvertName(descriptor.PropertyName); + } + return descriptor => descriptor.PropertyName; } - - return descriptor => descriptor.PropertyName; } public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) diff --git a/test/Protobuf.System.Text.Json.Tests/JsonNamingPolicyTests.Should_use_protobuf_field_name_when_PropertyNamingSource_set_to_ProtobufFieldName.approved.json b/test/Protobuf.System.Text.Json.Tests/JsonNamingPolicyTests.Should_use_protobuf_field_name_when_PropertyNamingSource_set_to_ProtobufFieldName.approved.json new file mode 100644 index 0000000..712f9d9 --- /dev/null +++ b/test/Protobuf.System.Text.Json.Tests/JsonNamingPolicyTests.Should_use_protobuf_field_name_when_PropertyNamingSource_set_to_ProtobufFieldName.approved.json @@ -0,0 +1,17 @@ +{ + "double_property": 2.5, + "float_property": 0, + "int_32_property": 0, + "int_64_property": 0, + "uint_32_property": 0, + "uint_64_property": 0, + "sint_32_property": 0, + "sint_64_property": 0, + "fixed_32_property": 0, + "fixed_64_property": 0, + "sfixed_32_property": 0, + "sfixed_64_property": 0, + "bool_property": false, + "string_property": "", + "bytes_property": "" +} \ No newline at end of file diff --git a/test/Protobuf.System.Text.Json.Tests/JsonNamingPolicyTests.Should_use_protobuf_json_name_when_PropertyNamingSource_set_to_ProtobufJsonName.approved.json b/test/Protobuf.System.Text.Json.Tests/JsonNamingPolicyTests.Should_use_protobuf_json_name_when_PropertyNamingSource_set_to_ProtobufJsonName.approved.json new file mode 100644 index 0000000..1283b7d --- /dev/null +++ b/test/Protobuf.System.Text.Json.Tests/JsonNamingPolicyTests.Should_use_protobuf_json_name_when_PropertyNamingSource_set_to_ProtobufJsonName.approved.json @@ -0,0 +1,17 @@ +{ + "doubleProperty": 2.5, + "floatProperty": 0, + "int32Property": 0, + "int64Property": 0, + "uint32Property": 0, + "uint64Property": 0, + "sint32Property": 0, + "sint64Property": 0, + "fixed32Property": 0, + "fixed64Property": 0, + "sfixed32Property": 0, + "sfixed64Property": 0, + "boolProperty": false, + "stringProperty": "", + "bytesProperty": "" +} \ No newline at end of file diff --git a/test/Protobuf.System.Text.Json.Tests/JsonNamingPolicyTests.cs b/test/Protobuf.System.Text.Json.Tests/JsonNamingPolicyTests.cs index 0d01dcf..a73aab9 100644 --- a/test/Protobuf.System.Text.Json.Tests/JsonNamingPolicyTests.cs +++ b/test/Protobuf.System.Text.Json.Tests/JsonNamingPolicyTests.cs @@ -37,7 +37,9 @@ public void Should_ignore_PropertyNamingPolicy_when_UseProtobufJsonNames_set_to_ }; var jsonSerializerOptions = new JsonSerializerOptions(); jsonSerializerOptions.PropertyNamingPolicy = new JsonLowerCaseNamingPolicy(); +#pragma warning disable CS0618 // Type or member is obsolete jsonSerializerOptions.AddProtobufSupport(options => options.UseProtobufJsonNames = true); +#pragma warning restore CS0618 // Type or member is obsolete // Act var serialized = JsonSerializer.Serialize(msg, jsonSerializerOptions); @@ -47,6 +49,62 @@ public void Should_ignore_PropertyNamingPolicy_when_UseProtobufJsonNames_set_to_ approver.VerifyJson(serialized); } + [Fact] + public void Should_use_protobuf_json_name_when_PropertyNamingSource_set_to_ProtobufJsonName() + { + // Arrange + var msg = new SimpleMessage + { + DoubleProperty = 2.5d + }; + var jsonSerializerOptions = new JsonSerializerOptions(); + jsonSerializerOptions.PropertyNamingPolicy = new JsonLowerCaseNamingPolicy(); + jsonSerializerOptions.AddProtobufSupport(options => options.PropertyNamingSource = PropertyNamingSource.ProtobufJsonName); + + // Act + var serialized = JsonSerializer.Serialize(msg, jsonSerializerOptions); + + // Assert + var approver = new ExplicitApprover(); + approver.VerifyJson(serialized); + } + + [Fact] + public void Should_use_protobuf_field_name_when_PropertyNamingSource_set_to_ProtobufFieldName() + { + // Arrange + var msg = new SimpleMessage + { + DoubleProperty = 2.5d + }; + var jsonSerializerOptions = new JsonSerializerOptions(); + jsonSerializerOptions.PropertyNamingPolicy = new JsonLowerCaseNamingPolicy(); + jsonSerializerOptions.AddProtobufSupport(options => options.PropertyNamingSource = PropertyNamingSource.ProtobufFieldName); + + // Act + var serialized = JsonSerializer.Serialize(msg, jsonSerializerOptions); + + // Assert + var approver = new ExplicitApprover(); + approver.VerifyJson(serialized); + } + + [Fact] + public void Should_deserialize_using_protobuf_field_name_when_PropertyNamingSource_set_to_ProtobufFieldName() + { + // Arrange + var json = "{\"double_property\": 2.5}"; + var jsonSerializerOptions = new JsonSerializerOptions(); + jsonSerializerOptions.AddProtobufSupport(options => options.PropertyNamingSource = PropertyNamingSource.ProtobufFieldName); + + // Act + var deserialized = JsonSerializer.Deserialize(json, jsonSerializerOptions); + + // Assert + Assert.NotNull(deserialized); + Assert.Equal(2.5d, deserialized.DoubleProperty); + } + private class JsonLowerCaseNamingPolicy : JsonNamingPolicy { public override string ConvertName(string name) diff --git a/test/Protobuf.System.Text.Json.Tests/PropertyNamingSourceTests.Should_use_custom_json_name_when_PropertyNamingSource_is_ProtobufJsonName.approved.json b/test/Protobuf.System.Text.Json.Tests/PropertyNamingSourceTests.Should_use_custom_json_name_when_PropertyNamingSource_is_ProtobufJsonName.approved.json new file mode 100644 index 0000000..bfbb690 --- /dev/null +++ b/test/Protobuf.System.Text.Json.Tests/PropertyNamingSourceTests.Should_use_custom_json_name_when_PropertyNamingSource_is_ProtobufJsonName.approved.json @@ -0,0 +1,4 @@ +{ + "customDoubleProperty": 2.5, + "stringProperty": "test" +} \ No newline at end of file diff --git a/test/Protobuf.System.Text.Json.Tests/PropertyNamingSourceTests.Should_use_proto_field_name_when_PropertyNamingSource_is_ProtobufFieldName.approved.json b/test/Protobuf.System.Text.Json.Tests/PropertyNamingSourceTests.Should_use_proto_field_name_when_PropertyNamingSource_is_ProtobufFieldName.approved.json new file mode 100644 index 0000000..e0d28fc --- /dev/null +++ b/test/Protobuf.System.Text.Json.Tests/PropertyNamingSourceTests.Should_use_proto_field_name_when_PropertyNamingSource_is_ProtobufFieldName.approved.json @@ -0,0 +1,4 @@ +{ + "double_property": 2.5, + "string_property": "test" +} \ No newline at end of file diff --git a/test/Protobuf.System.Text.Json.Tests/PropertyNamingSourceTests.cs b/test/Protobuf.System.Text.Json.Tests/PropertyNamingSourceTests.cs new file mode 100644 index 0000000..0db3dc9 --- /dev/null +++ b/test/Protobuf.System.Text.Json.Tests/PropertyNamingSourceTests.cs @@ -0,0 +1,105 @@ +using System.Text.Json; +using System.Text.Json.Protobuf.Tests; +using SmartAnalyzers.ApprovalTestsExtensions; +using Xunit; + +namespace Protobuf.System.Text.Json.Tests; + +public class PropertyNamingSourceTests +{ + [Fact] + public void Should_use_custom_json_name_when_PropertyNamingSource_is_ProtobufJsonName() + { + // Arrange + var msg = new MessageWithCustomJsonName + { + DoubleProperty = 2.5d, + StringProperty = "test" + }; + var jsonSerializerOptions = new JsonSerializerOptions(); + jsonSerializerOptions.AddProtobufSupport(options => options.PropertyNamingSource = PropertyNamingSource.ProtobufJsonName); + + // Act + var serialized = JsonSerializer.Serialize(msg, jsonSerializerOptions); + + // Assert + var approver = new ExplicitApprover(); + approver.VerifyJson(serialized); + } + + [Fact] + public void Should_use_proto_field_name_when_PropertyNamingSource_is_ProtobufFieldName() + { + // Arrange + var msg = new MessageWithCustomJsonName + { + DoubleProperty = 2.5d, + StringProperty = "test" + }; + var jsonSerializerOptions = new JsonSerializerOptions(); + jsonSerializerOptions.AddProtobufSupport(options => options.PropertyNamingSource = PropertyNamingSource.ProtobufFieldName); + + // Act + var serialized = JsonSerializer.Serialize(msg, jsonSerializerOptions); + + // Assert + var approver = new ExplicitApprover(); + approver.VerifyJson(serialized); + } + + [Fact] + public void Should_deserialize_using_custom_json_name_when_PropertyNamingSource_is_ProtobufJsonName() + { + // Arrange + var json = "{\"customDoubleProperty\": 2.5, \"stringProperty\": \"test\"}"; + var jsonSerializerOptions = new JsonSerializerOptions(); + jsonSerializerOptions.AddProtobufSupport(options => options.PropertyNamingSource = PropertyNamingSource.ProtobufJsonName); + + // Act + var deserialized = JsonSerializer.Deserialize(json, jsonSerializerOptions); + + // Assert + Assert.NotNull(deserialized); + Assert.Equal(2.5d, deserialized.DoubleProperty); + Assert.Equal("test", deserialized.StringProperty); + } + + [Fact] + public void Should_deserialize_using_proto_field_name_when_PropertyNamingSource_is_ProtobufFieldName() + { + // Arrange + var json = "{\"double_property\": 2.5, \"string_property\": \"test\"}"; + var jsonSerializerOptions = new JsonSerializerOptions(); + jsonSerializerOptions.AddProtobufSupport(options => options.PropertyNamingSource = PropertyNamingSource.ProtobufFieldName); + + // Act + var deserialized = JsonSerializer.Deserialize(json, jsonSerializerOptions); + + // Assert + Assert.NotNull(deserialized); + Assert.Equal(2.5d, deserialized.DoubleProperty); + Assert.Equal("test", deserialized.StringProperty); + } + + [Fact] + public void Should_round_trip_with_ProtobufFieldName() + { + // Arrange + var original = new MessageWithCustomJsonName + { + DoubleProperty = 2.5d, + StringProperty = "test" + }; + var jsonSerializerOptions = new JsonSerializerOptions(); + jsonSerializerOptions.AddProtobufSupport(options => options.PropertyNamingSource = PropertyNamingSource.ProtobufFieldName); + + // Act + var serialized = JsonSerializer.Serialize(original, jsonSerializerOptions); + var deserialized = JsonSerializer.Deserialize(serialized, jsonSerializerOptions); + + // Assert + Assert.NotNull(deserialized); + Assert.Equal(original.DoubleProperty, deserialized.DoubleProperty); + Assert.Equal(original.StringProperty, deserialized.StringProperty); + } +} diff --git a/test/Protobuf.System.Text.Json.Tests/Protos/message_with_custom_json_name.proto b/test/Protobuf.System.Text.Json.Tests/Protos/message_with_custom_json_name.proto new file mode 100644 index 0000000..eb57a38 --- /dev/null +++ b/test/Protobuf.System.Text.Json.Tests/Protos/message_with_custom_json_name.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +option csharp_namespace = "System.Text.Json.Protobuf.Tests"; + +message MessageWithCustomJsonName { + double double_property = 1 [json_name = "customDoubleProperty"]; + + string string_property = 2; +}