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+)?$"