diff --git a/src/OptionalValues/OptionalValueJsonExtensions.cs b/src/OptionalValues/OptionalValueJsonExtensions.cs index 2ef3f5e..24cff3e 100644 --- a/src/OptionalValues/OptionalValueJsonExtensions.cs +++ b/src/OptionalValues/OptionalValueJsonExtensions.cs @@ -11,6 +11,7 @@ public static class OptionalValueJsonExtensions /// /// Modifies the provided to add support for . /// + /// This should prefereably be done as the last call, as it applies a modifier to the registered instances in the TypeInfoResolverChain. /// The to modify. /// The modified to allow for chaining. /// Thrown when is . @@ -18,8 +19,16 @@ public static JsonSerializerOptions AddOptionalValueSupport(this JsonSerializerO { ArgumentNullException.ThrowIfNull(options); - options.TypeInfoResolver = (options.TypeInfoResolver ?? new DefaultJsonTypeInfoResolver()) - .WithAddedModifier(OptionalValueJsonTypeInfoResolverModifier.ModifyTypeInfo); + // If the options do not have a TypeInfoResolver, add the default one, with the modifier. + options.TypeInfoResolver ??= new DefaultJsonTypeInfoResolver(); + + // We need to add the modifier to all resolvers in the chain, + // because it needs to be applied to all types and it's properties. + for (var i = 0; i < options.TypeInfoResolverChain.Count; i++) + { + options.TypeInfoResolverChain[i] = options.TypeInfoResolverChain[i] + .WithAddedModifier(OptionalValueJsonTypeInfoResolverModifier.ModifyTypeInfo); + } return options; } @@ -29,7 +38,7 @@ public static JsonSerializerOptions AddOptionalValueSupport(this JsonSerializerO /// /// The base options to copy. /// A new based on the provided options with support for . - public static JsonSerializerOptions WithOptionalValueSupport(this JsonSerializerOptions options) => - new JsonSerializerOptions(options) + public static JsonSerializerOptions WithOptionalValueSupport(this JsonSerializerOptions options) + => new JsonSerializerOptions(options) .AddOptionalValueSupport(); } \ No newline at end of file diff --git a/test/OptionalValues.Tests/OptionalValueJsonWithSourceGeneratorTest.cs b/test/OptionalValues.Tests/OptionalValueJsonWithSourceGeneratorTest.cs new file mode 100644 index 0000000..8c90877 --- /dev/null +++ b/test/OptionalValues.Tests/OptionalValueJsonWithSourceGeneratorTest.cs @@ -0,0 +1,184 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace OptionalValues.Tests; + +public class OptionalValueJsonWithSourceGeneratorTest +{ + private static JsonSerializerOptions CreateOptionsSingleContext() + { + var options = new JsonSerializerOptions + { + TypeInfoResolver = OptionalValueJsonWithSourceGeneratorJsonSerializationContext.Default + }; + options.AddOptionalValueSupport(); + return options; + } + + private static JsonSerializerOptions CreateOptionsMultipleContexts() + { + var options = new JsonSerializerOptions + { + TypeInfoResolverChain = + { + OptionalValueJsonWithSourceGeneratorJsonSerializationContext.Default, + OtherOptionalValueJsonWithSourceGeneratorJsonSerializationContext.Default + } + }; + // This needs to be done last, because it will add modifiers to all resolvers in the chain. + options.AddOptionalValueSupport(); + return options; + } + + [Fact] + public void SerializeWithValues_ShouldWriteValues() + { + var model = new TestModel + { + Name = new OptionalValue("John"), + Age = new OptionalValue(42) + }; + + var options = CreateOptionsSingleContext(); + var json = JsonSerializer.Serialize(model, options); + + Assert.Equal("""{"Name":"John","Age":42}""", json); + } + + [Fact] + public void SerializeWithUnspecified_ShouldNotWriteValues() + { + var model = new TestModel + { + Name = OptionalValue.Unspecified, + Age = OptionalValue.Unspecified + }; + + var options = CreateOptionsSingleContext(); + var json = JsonSerializer.Serialize(model, options); + + Assert.Equal("{}", json); + } + + [Fact] + public void DeserializeWithValues_ShouldReadValues() + { + var json = """{"Name":"John","Age":42}"""; + + var options = CreateOptionsSingleContext(); + var model = JsonSerializer.Deserialize(json, options); + + Assert.Equal("John", model.Name.Value); + Assert.Equal(42, model.Age.Value); + } + + [Fact] + public void DeserializeWithUnspecified_ShouldReadUnspecified() + { + var json = "{}"; + + var options = CreateOptionsSingleContext(); + var model = JsonSerializer.Deserialize(json, options); + + Assert.False(model.Name.IsSpecified); + Assert.False(model.Age.IsSpecified); + } + + [Fact] + public void SerializeWithValuesMultipleContexts_ShouldWriteValues() + { + var model = new TestModelOtherContext + { + Test = new TestModel + { + Name = new OptionalValue("John"), + Age = new OptionalValue(42) + }, + Street = new OptionalValue("Main Street"), + HouseNumber = new OptionalValue(42) + }; + + var options = CreateOptionsMultipleContexts(); + var json = JsonSerializer.Serialize(model, options); + + Assert.Equal("""{"Test":{"Name":"John","Age":42},"Street":"Main Street","HouseNumber":42}""", json); + } + + [Fact] + public void SerializeWithUnspecifiedMultipleContexts_ShouldNotWriteValues() + { + var model = new TestModelOtherContext + { + Test = new TestModel + { + Name = OptionalValue.Unspecified, + Age = OptionalValue.Unspecified + }, + Street = OptionalValue.Unspecified, + HouseNumber = OptionalValue.Unspecified + }; + + var options = CreateOptionsMultipleContexts(); + var json = JsonSerializer.Serialize(model, options); + + Assert.Equal("""{"Test":{}}""", json); + } + + [Fact] + public void DeserializeWithValuesMultipleContexts_ShouldReadValues() + { + var json = """{"Test":{"Name":"John","Age":42},"Street":"Main Street","HouseNumber":42}"""; + + var options = CreateOptionsMultipleContexts(); + var model = JsonSerializer.Deserialize(json, options); + + Assert.Equal("John", model.Test.Value.Name.Value); + Assert.Equal(42, model.Test.Value.Age.Value); + Assert.Equal("Main Street", model.Street.Value); + Assert.Equal(42, model.HouseNumber.Value); + } + + [Fact] + public void DeserializeWithUnspecifiedMultipleContexts_ShouldReadUnspecified() + { + var json = """{"Test":{}}"""; + + var options = CreateOptionsMultipleContexts(); + var model = JsonSerializer.Deserialize(json, options); + + Assert.True(model.Test.IsSpecified); + + Assert.False(model.Test.SpecifiedValue.Name.IsSpecified); + Assert.False(model.Test.SpecifiedValue.Age.IsSpecified); + Assert.False(model.Street.IsSpecified); + Assert.False(model.HouseNumber.IsSpecified); + } + + public class TestModel + { + public OptionalValue Name { get; set; } + + public OptionalValue Age { get; set; } + } + + public class TestModelOtherContext + { + public OptionalValue Test { get; set; } + + public OptionalValue Street { get; set; } + + public OptionalValue HouseNumber { get; set; } + } +} + +[JsonSerializable(typeof(OptionalValueJsonWithSourceGeneratorTest.TestModel))] +[JsonSerializable(typeof(int))] +[JsonSerializable(typeof(string))] +public partial class OptionalValueJsonWithSourceGeneratorJsonSerializationContext : JsonSerializerContext +{ +} + +[JsonSerializable(typeof(OptionalValueJsonWithSourceGeneratorTest.TestModelOtherContext))] +public partial class OtherOptionalValueJsonWithSourceGeneratorJsonSerializationContext : JsonSerializerContext +{ +} \ No newline at end of file