Skip to content

Commit

Permalink
Apply the JsonTypeInfoModifier to all TypeInfoResolvers in the chain.
Browse files Browse the repository at this point in the history
Doing this allows supporting multiple JsonSerializerContexts, which is often the case when using Json Source Generation.
  • Loading branch information
desjoerd committed Nov 28, 2024
1 parent 3f5a646 commit 98a5679
Show file tree
Hide file tree
Showing 2 changed files with 197 additions and 4 deletions.
17 changes: 13 additions & 4 deletions src/OptionalValues/OptionalValueJsonExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,24 @@ public static class OptionalValueJsonExtensions
/// <summary>
/// Modifies the provided <see cref="JsonSerializerOptions"/> to add support for <see cref="OptionalValue{T}"/>.
/// </summary>
/// <remarks>This should prefereably be done as the last call, as it applies a modifier to the registered <see cref="JsonTypeInfoResolver"/> instances in the TypeInfoResolverChain.</remarks>
/// <param name="options">The <see cref="JsonSerializerOptions"/> to modify.</param>
/// <returns>The modified <see cref="JsonSerializerOptions"/> to allow for chaining.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="options"/> is <see langword="null"/>.</exception>
public static JsonSerializerOptions AddOptionalValueSupport(this JsonSerializerOptions options)
{
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;
}
Expand All @@ -29,7 +38,7 @@ public static JsonSerializerOptions AddOptionalValueSupport(this JsonSerializerO
/// </summary>
/// <param name="options">The base <see cref="JsonSerializerOptions"/> options to copy.</param>
/// <returns>A new <see cref="JsonSerializerOptions"/> based on the provided options with support for <see cref="OptionalValue{T}"/>.</returns>
public static JsonSerializerOptions WithOptionalValueSupport(this JsonSerializerOptions options) =>
new JsonSerializerOptions(options)
public static JsonSerializerOptions WithOptionalValueSupport(this JsonSerializerOptions options)
=> new JsonSerializerOptions(options)
.AddOptionalValueSupport();
}
184 changes: 184 additions & 0 deletions test/OptionalValues.Tests/OptionalValueJsonWithSourceGeneratorTest.cs
Original file line number Diff line number Diff line change
@@ -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<string>("John"),
Age = new OptionalValue<int>(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<string>.Unspecified,
Age = OptionalValue<int>.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<TestModel>(json, options);

Assert.Equal("John", model.Name.Value);

Check warning on line 71 in test/OptionalValues.Tests/OptionalValueJsonWithSourceGeneratorTest.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

Check warning on line 71 in test/OptionalValues.Tests/OptionalValueJsonWithSourceGeneratorTest.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
Assert.Equal(42, model.Age.Value);
}

[Fact]
public void DeserializeWithUnspecified_ShouldReadUnspecified()
{
var json = "{}";

var options = CreateOptionsSingleContext();
var model = JsonSerializer.Deserialize<TestModel>(json, options);

Assert.False(model.Name.IsSpecified);

Check warning on line 83 in test/OptionalValues.Tests/OptionalValueJsonWithSourceGeneratorTest.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

Check warning on line 83 in test/OptionalValues.Tests/OptionalValueJsonWithSourceGeneratorTest.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
Assert.False(model.Age.IsSpecified);
}

[Fact]
public void SerializeWithValuesMultipleContexts_ShouldWriteValues()
{
var model = new TestModelOtherContext
{
Test = new TestModel
{
Name = new OptionalValue<string>("John"),
Age = new OptionalValue<int>(42)
},
Street = new OptionalValue<string>("Main Street"),
HouseNumber = new OptionalValue<int>(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<string>.Unspecified,
Age = OptionalValue<int>.Unspecified
},
Street = OptionalValue<string>.Unspecified,
HouseNumber = OptionalValue<int>.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<TestModelOtherContext>(json, options);

Assert.Equal("John", model.Test.Value.Name.Value);

Check warning on line 135 in test/OptionalValues.Tests/OptionalValueJsonWithSourceGeneratorTest.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

Check warning on line 135 in test/OptionalValues.Tests/OptionalValueJsonWithSourceGeneratorTest.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

Check warning on line 135 in test/OptionalValues.Tests/OptionalValueJsonWithSourceGeneratorTest.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

Check warning on line 135 in test/OptionalValues.Tests/OptionalValueJsonWithSourceGeneratorTest.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
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<TestModelOtherContext>(json, options);

Assert.True(model.Test.IsSpecified);

Check warning on line 149 in test/OptionalValues.Tests/OptionalValueJsonWithSourceGeneratorTest.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

Check warning on line 149 in test/OptionalValues.Tests/OptionalValueJsonWithSourceGeneratorTest.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

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<string> Name { get; set; }

public OptionalValue<int> Age { get; set; }
}

public class TestModelOtherContext
{
public OptionalValue<TestModel> Test { get; set; }

public OptionalValue<string> Street { get; set; }

public OptionalValue<int> 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
{
}

0 comments on commit 98a5679

Please sign in to comment.