diff --git a/src/OptionalValues/OptionalValueJsonConverterAttribute.cs b/src/OptionalValues/OptionalValueJsonConverterAttribute.cs new file mode 100644 index 0000000..0cfbd9c --- /dev/null +++ b/src/OptionalValues/OptionalValueJsonConverterAttribute.cs @@ -0,0 +1,64 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Text.Json.Serialization; + +namespace OptionalValues; + +/// +/// When placed on a property or field of type , specifies the converter type to use for T. +/// +/// +/// The specified converter type must derive from JsonConverter. +/// +[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 +{ + /// + /// Initializes a new instance of with the specified inner converter type. + /// + /// + public OptionalValueJsonConverterAttribute(Type innerConverterType) + { + InnerConverterType = innerConverterType; + } + + /// + /// Protected constructor for derived classes, allowing to create custom logic for creating the inner converter. + /// + protected OptionalValueJsonConverterAttribute() + { + } + + /// + /// Gets the type of the inner converter. This can be null if is overridden to provide custom logic for creating the inner converter. + /// + public Type? InnerConverterType { get; } + + /// + 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 + ); + } + + /// + /// Creates the inner converter. This method can be overridden in derived classes to provide custom logic for creating the inner converter. + /// + /// A or + 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)!; + } +} \ No newline at end of file diff --git a/src/OptionalValues/OptionalValueJsonConverterFactory.cs b/src/OptionalValues/OptionalValueJsonConverterFactory.cs index e82373c..9189f8b 100644 --- a/src/OptionalValues/OptionalValueJsonConverterFactory.cs +++ b/src/OptionalValues/OptionalValueJsonConverterFactory.cs @@ -13,6 +13,20 @@ namespace OptionalValues; Justification = "Class is instantiated via reflection by JsonSerializer.")] public sealed class OptionalValueJsonConverterFactory : JsonConverterFactory { + private readonly JsonConverter? _customInnerConverter; + + /// + /// Initializes a new instance of the class. + /// + public OptionalValueJsonConverterFactory() + { + } + + internal OptionalValueJsonConverterFactory(JsonConverter? customInnerConverter) + { + _customInnerConverter = customInnerConverter; + } + /// public override bool CanConvert(Type typeToConvert) => OptionalValue.IsOptionalValueType(typeToConvert); @@ -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 for the given T + // Create the specific OptionalValueJsonConverter for the given T var converter = (JsonConverter)Activator.CreateInstance( typeof(OptionalValueJsonConverter<>).MakeGenericType(valueType), inner )!; diff --git a/src/OptionalValues/PublicAPI.Unshipped.txt b/src/OptionalValues/PublicAPI.Unshipped.txt index e69de29..6459d04 100644 --- a/src/OptionalValues/PublicAPI.Unshipped.txt +++ b/src/OptionalValues/PublicAPI.Unshipped.txt @@ -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! \ No newline at end of file diff --git a/test/OptionalValues.Tests/OptionalValueJsonConverterAttributeTest.cs b/test/OptionalValues.Tests/OptionalValueJsonConverterAttributeTest.cs new file mode 100644 index 0000000..ee196da --- /dev/null +++ b/test/OptionalValues.Tests/OptionalValueJsonConverterAttributeTest.cs @@ -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))] + public OptionalValue 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.Foo) + }; + + var json = JsonSerializer.Serialize(model, JsonSerializerOptions); + Assert.Equal("""{"EnumValue":"Foo"}""", json); + } + + [Fact] + public void ShouldDeserializeWithCustomConverterOnProperty() + { + var json = """{"EnumValue":"Bar"}"""; + ExampleModel model = JsonSerializer.Deserialize(json, JsonSerializerOptions)!; + + Assert.Equal(ExampleEnum.Bar, model.EnumValue.Value); + } +} \ No newline at end of file diff --git a/version.json b/version.json index 8d34c0c..b0dd3e1 100644 --- a/version.json +++ b/version.json @@ -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+)?$"