Skip to content

Commit

Permalink
✨ Add support for setting a different JsonConverter on properties
Browse files Browse the repository at this point in the history
  • Loading branch information
desjoerd committed Jan 5, 2025
1 parent dca8046 commit e9266bf
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 3 deletions.
64 changes: 64 additions & 0 deletions src/OptionalValues/OptionalValueJsonConverterAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text.Json.Serialization;

namespace OptionalValues;

/// <summary>
/// When placed on a property or field of type <see cref="OptionalValue{T}"/>, specifies the converter type to use for <c>T</c>.
/// </summary>
/// <remarks>
/// The specified converter type must derive from JsonConverter.
/// </remarks>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
[SuppressMessage("Performance", "CA1813:Avoid unsealed attributes", Justification = "This attribute should be inheritable to allow custom initialization logic.")]
public class OptionalValueJsonConverterAttribute : JsonConverterAttribute
{
/// <summary>
/// Initializes a new instance of <see cref="OptionalValueJsonConverterAttribute"/> with the specified inner converter type.
/// </summary>
/// <param name="innerConverterType"></param>
public OptionalValueJsonConverterAttribute(Type innerConverterType)
{
InnerConverterType = innerConverterType;
}

/// <summary>
/// Protected constructor for derived classes, allowing to create custom logic for creating the inner converter.
/// </summary>
protected OptionalValueJsonConverterAttribute()
{
}

/// <summary>
/// Gets the type of the inner converter. This can be null if <see cref="CreateInnerConverter"/> is overridden to provide custom logic for creating the inner converter.
/// </summary>
public Type? InnerConverterType { get; }

/// <inheritdoc />
public override JsonConverter? CreateConverter(Type typeToConvert)
{
JsonConverter innerConverter = CreateInnerConverter();

return (JsonConverter?)Activator.CreateInstance(
type: typeof(OptionalValueJsonConverterFactory),
bindingAttr: BindingFlags.NonPublic | BindingFlags.Instance,
args: [innerConverter],
binder: null,
culture: null
);
}

/// <summary>
/// Creates the inner converter. This method can be overridden in derived classes to provide custom logic for creating the inner converter.
/// </summary>
/// <returns>A <see cref="JsonConverter{T}"/> or <see cref="JsonConverterFactory"/></returns>
protected virtual JsonConverter CreateInnerConverter()
{
if (InnerConverterType is null)
{
throw new InvalidOperationException("When inheriting from OptionalValueJsonConverterAttribute, the CreateInnerConverter method must be overridden.");
}
return (JsonConverter)Activator.CreateInstance(InnerConverterType)!;
}
}
30 changes: 28 additions & 2 deletions src/OptionalValues/OptionalValueJsonConverterFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,20 @@ namespace OptionalValues;
Justification = "Class is instantiated via reflection by JsonSerializer.")]
public sealed class OptionalValueJsonConverterFactory : JsonConverterFactory
{
private readonly JsonConverter? _customInnerConverter;

/// <summary>
/// Initializes a new instance of the <see cref="OptionalValueJsonConverterFactory"/> class.
/// </summary>
public OptionalValueJsonConverterFactory()
{
}

internal OptionalValueJsonConverterFactory(JsonConverter? customInnerConverter)
{
_customInnerConverter = customInnerConverter;
}

/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
=> OptionalValue.IsOptionalValueType(typeToConvert);
Expand All @@ -23,9 +37,21 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer
{
Type valueType = OptionalValue.GetUnderlyingType(typeToConvert);

JsonConverter inner = options.GetConverter(valueType);
JsonConverter? inner = _customInnerConverter ?? options.GetConverter(valueType);
if (inner is JsonConverterFactory factory)
{
inner = factory.CreateConverter(valueType, options);
}

switch (inner)
{
case null:
throw new InvalidOperationException($"No converter found for {valueType}.");
case JsonConverterFactory:
throw new InvalidOperationException($"Converter for {valueType} is a factory which returned a factory.");
}

// Create the specific DefinedJsonConverter<T> for the given T
// Create the specific OptionalValueJsonConverter<T> for the given T
var converter = (JsonConverter)Activator.CreateInstance(
typeof(OptionalValueJsonConverter<>).MakeGenericType(valueType), inner
)!;
Expand Down
6 changes: 6 additions & 0 deletions src/OptionalValues/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
OptionalValues.OptionalValueJsonConverterAttribute
OptionalValues.OptionalValueJsonConverterAttribute.InnerConverterType.get -> System.Type?
OptionalValues.OptionalValueJsonConverterAttribute.OptionalValueJsonConverterAttribute() -> void
OptionalValues.OptionalValueJsonConverterAttribute.OptionalValueJsonConverterAttribute(System.Type! innerConverterType) -> void
override OptionalValues.OptionalValueJsonConverterAttribute.CreateConverter(System.Type! typeToConvert) -> System.Text.Json.Serialization.JsonConverter?
virtual OptionalValues.OptionalValueJsonConverterAttribute.CreateInnerConverter() -> System.Text.Json.Serialization.JsonConverter!
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace OptionalValues.Tests;

public class OptionalValueJsonConverterAttributeTest
{
public class ExampleModel
{
[OptionalValueJsonConverter(typeof(JsonStringEnumConverter<ExampleEnum>))]
public OptionalValue<ExampleEnum> EnumValue { get; set; }
}

public enum ExampleEnum
{
Foo,
Bar
}

private static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions()
.AddOptionalValueSupport();

[Fact]
public void ShouldSerializeWithCustomConverterOnProperty()
{
var model = new ExampleModel
{
EnumValue = new OptionalValue<ExampleEnum>(ExampleEnum.Foo)
};

var json = JsonSerializer.Serialize(model, JsonSerializerOptions);
Assert.Equal("""{"EnumValue":"Foo"}""", json);
}

[Fact]
public void ShouldDeserializeWithCustomConverterOnProperty()
{
var json = """{"EnumValue":"Bar"}""";
ExampleModel model = JsonSerializer.Deserialize<ExampleModel>(json, JsonSerializerOptions)!;

Assert.Equal(ExampleEnum.Bar, model.EnumValue.Value);
}
}
2 changes: 1 addition & 1 deletion version.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
"version": "0.2",
"version": "0.3",
"publicReleaseRefSpec": [
"^refs/heads/main$",
"^refs/heads/v\\d+(?:\\.\\d+)?$"
Expand Down

0 comments on commit e9266bf

Please sign in to comment.