Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,26 @@ var deserialized = JsonSerializer.Deserialize<SimpleMessage>(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
Expand Down
13 changes: 12 additions & 1 deletion src/Protobuf.System.Text.Json/JsonProtobufSerializerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,18 @@ public class JsonProtobufSerializerOptions
/// option in the .proto file.
/// The default value is false.
/// </summary>
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;
}

/// <summary>
/// Specifies the source for property names in JSON serialization.
/// The default value is <see cref="PropertyNamingSource.Default"/>.
/// </summary>
public PropertyNamingSource PropertyNamingSource { get; set; } = PropertyNamingSource.Default;

/// <summary>
/// Controls how <see cref="Google.Protobuf.WellKnownTypes.Duration"/> fields are handled.
Expand Down
25 changes: 25 additions & 0 deletions src/Protobuf.System.Text.Json/PropertyNamingSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace Protobuf.System.Text.Json;

/// <summary>
/// Specifies the source for property names in JSON serialization.
/// </summary>
public enum PropertyNamingSource
{
/// <summary>
/// Use the default PropertyNamingPolicy from JsonSerializerOptions.
/// </summary>
Default = 0,

/// <summary>
/// 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.
/// </summary>
ProtobufJsonName = 1,

/// <summary>
/// Use the original field name as defined in the .proto file.
/// For example, "double_property" instead of "doubleProperty".
/// </summary>
ProtobufFieldName = 2
}
27 changes: 16 additions & 11 deletions src/Protobuf.System.Text.Json/ProtobufConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
{
Expand All @@ -49,19 +49,24 @@ public ProtobufConverter(JsonSerializerOptions jsonSerializerOptions, JsonProtob
_fieldsLookup = _fields.ToDictionary(x => x.JsonName, x => x, stringComparer);
}

private static Func<FieldDescriptor, string> GetConvertNameFunc(JsonNamingPolicy? jsonNamingPolicy, bool useProtobufJsonNames)
private static Func<FieldDescriptor, string> 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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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": ""
}
Original file line number Diff line number Diff line change
@@ -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": ""
}
58 changes: 58 additions & 0 deletions test/Protobuf.System.Text.Json.Tests/JsonNamingPolicyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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<SimpleMessage>(json, jsonSerializerOptions);

// Assert
Assert.NotNull(deserialized);
Assert.Equal(2.5d, deserialized.DoubleProperty);
}

private class JsonLowerCaseNamingPolicy : JsonNamingPolicy
{
public override string ConvertName(string name)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"customDoubleProperty": 2.5,
"stringProperty": "test"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"double_property": 2.5,
"string_property": "test"
}
105 changes: 105 additions & 0 deletions test/Protobuf.System.Text.Json.Tests/PropertyNamingSourceTests.cs
Original file line number Diff line number Diff line change
@@ -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<MessageWithCustomJsonName>(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<MessageWithCustomJsonName>(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<MessageWithCustomJsonName>(serialized, jsonSerializerOptions);

// Assert
Assert.NotNull(deserialized);
Assert.Equal(original.DoubleProperty, deserialized.DoubleProperty);
Assert.Equal(original.StringProperty, deserialized.StringProperty);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}