Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Nov 11, 2025

Validates that Fluid's json filter works correctly when users supply source-generated JsonSerializerContext metadata via TemplateOptions.JsonSerializerOptions.

Changes

  • New test file: Fluid.Tests/JsonSourceGenTests.cs
    • Source-generated context FluidJsonContext with camelCase naming policy
    • Model classes SourceGenPerson and SourceGenAddress for test scenarios
    • 4 test cases covering objects, arrays, dictionaries, FluidValue wrapping, and naming policy

Usage Pattern

var ctx = new TemplateContext(new TemplateOptions
{
    JsonSerializerOptions = new JsonSerializerOptions
    {
        TypeInfoResolver = JsonTypeInfoResolver.Combine(
            MySourceGenContext.Default, 
            new DefaultJsonTypeInfoResolver()
        ),
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase
    }
});

ctx.Options.Filters.WithMiscFilters();
ctx.SetValue("person", myPerson);
var template = FluidParser.Parse("{{ person | json }}");
var output = template.Render(ctx);  // Uses source-gen metadata with fallback to reflection

Key Implementation Details

  • Combines source-generated resolver with DefaultJsonTypeInfoResolver to support both user types and Fluid's internal FluidValue types
  • Naming policy must be set on JsonSerializerOptions itself, not just in source-gen context options
  • Tests validate output matches direct JsonSerializer.Serialize calls with same context
Original prompt

Summary

Add a new test suite to validate that Fluid's json filter and FluidValueJsonConverter work correctly when callers supply System.Text.Json source‑generated metadata via a JsonSerializerContext. Users reported that TemplateOptions.JsonSerializerOptions (and transitively TemplateContext.JsonSerializerOptions) did not support source generation scenarios. These tests demonstrate compatibility and help prevent regressions.

Rationale

Fluid integrates with System.Text.Json through TemplateOptions.JsonSerializerOptions and a custom [JsonConverter] on FluidValue. .NET source generation uses JsonSerializerContext instances added to JsonSerializerOptions.TypeInfoResolverChain (or older Add methods). If the converter or the json filter sidesteps or mutates options incorrectly, serialization might fall back to reflection or fail to locate generated metadata. The tests:

  • Ensure a source‑generated context can be attached and is honored.
  • Compare template output to direct JsonSerializer.Serialize calls using the same context.
  • Exercise primitive, complex, array and dictionary values, plus wrapped FluidValue instances.
  • Validate camelCase naming policy from source generation.

Added File

Create a new test file Fluid.Tests/JsonSourceGenTests.cs containing:

  • Source‑generated context FluidJsonContext with [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] and several [JsonSerializable] type declarations.
  • Model classes Person and Address used for nested serialization tests.
  • Four xUnit tests:
    1. JsonFilter_UsesSourceGeneratedContext_ForSimpleObject – end‑to‑end object serialization.
    2. JsonFilter_UsesSourceGen_ForArraysAndDictionaries – arrays and dictionaries.
    3. JsonFilter_FluidValueWrapping_DoesNotBreakSourceGen – direct FluidValue instances and mixed arrays.
    4. JsonFilter_RespectsCamelCaseNamingPolicy_FromSourceGen – asserts naming policy.

Test Implementation Notes

  • Tests target existing TFMs (net8.0, net9.0) defined in Fluid.Tests.csproj.
  • No project file changes are strictly required; source generation attributes work with the in-box System.Text.Json for these TFMs.
  • Uses TemplateOptions.MemberAccessStrategy.Register<T>() for model types and WithMiscFilters() extension to ensure the json filter is available.
  • Ensures Template.TryParse succeeds and reports errors if parsing fails.
  • Attaches the generated context via:
    var genOptions = new JsonSerializerOptions(FluidJsonContext.Default.Options);
    genOptions.TypeInfoResolverChain.Add(FluidJsonContext.Default);
    o.JsonSerializerOptions = genOptions;

Non-Goals

  • Performance benchmarking.
  • Encoder variations (e.g., UnsafeRelaxedJsonEscaping).
  • Indented formatting or DateTime specific tests.

Future Enhancements (Optional)

Additional tests could be added later for:

  • Custom JavaScriptEncoder behavior with source generation.
  • Indented output combined with source-gen context.
  • Large object graphs for performance validation.
  • DateTime / DateTimeOffset and custom converters registered alongside source‑gen.

File Content

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using Fluid;
using Fluid.Values;
using Xunit;

namespace Fluid.Tests
{
    [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
    [JsonSerializable(typeof(Person))]
    [JsonSerializable(typeof(Address))]
    [JsonSerializable(typeof(List<string>))]
    [JsonSerializable(typeof(Dictionary<string, int>))]
    [JsonSerializable(typeof(string[]))]
    public partial class FluidJsonContext : JsonSerializerContext { }

    public sealed class Person
    {
        public string FirstName { get; init; } = string.Empty;
        public string LastName { get; init; } = string.Empty;
        public int Age { get; init; }
        public Address HomeAddress { get; init; }
        public List<string> Tags { get; init; }
        public Dictionary<string, int> Scores { get; init; }
    }

    public sealed class Address
    {
        public string Street { get; init; } = string.Empty;
        public string City { get; init; } = string.Empty;
    }

    public class JsonSourceGenTests
    {
        private static Template Parse(string liquid)
        {
            Assert.True(Template.TryParse(liquid, out var template, out var errors), string.Join(Environment.NewLine, errors));
            return template;
        }

        private static TemplateContext CreateContext(Action<TemplateOptions> configureOptions = null)
        {
            var options = new TemplateOptions();
            options.MemberAccessStrategy = new DefaultMemberAccessStrategy();
            options.MemberAccessStrategy.Register<Person>();
            options.MemberAccessStrategy.Register<Address>();
            configureOptions?.Invoke(options);
            return new T...

</details>

*This pull request was created as a result of the following prompt from Copilot chat.*
> ### Summary
> Add a new test suite to validate that Fluid's `json` filter and `FluidValueJsonConverter` work correctly when callers supply `System.Text.Json` source‑generated metadata via a `JsonSerializerContext`. Users reported that `TemplateOptions.JsonSerializerOptions` (and transitively `TemplateContext.JsonSerializerOptions`) did not support source generation scenarios. These tests demonstrate compatibility and help prevent regressions.
> 
> ### Rationale
> Fluid integrates with `System.Text.Json` through `TemplateOptions.JsonSerializerOptions` and a custom `[JsonConverter]` on `FluidValue`. .NET source generation uses `JsonSerializerContext` instances added to `JsonSerializerOptions.TypeInfoResolverChain` (or older `Add` methods). If the converter or the `json` filter sidesteps or mutates options incorrectly, serialization might fall back to reflection or fail to locate generated metadata. The tests:
> - Ensure a source‑generated context can be attached and is honored.
> - Compare template output to direct `JsonSerializer.Serialize` calls using the same context.
> - Exercise primitive, complex, array and dictionary values, plus wrapped `FluidValue` instances.
> - Validate camelCase naming policy from source generation.
> 
> ### Added File
> Create a new test file `Fluid.Tests/JsonSourceGenTests.cs` containing:
> - Source‑generated context `FluidJsonContext` with `[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]` and several `[JsonSerializable]` type declarations.
> - Model classes `Person` and `Address` used for nested serialization tests.
> - Four xUnit tests:
>   1. `JsonFilter_UsesSourceGeneratedContext_ForSimpleObject` – end‑to‑end object serialization.
>   2. `JsonFilter_UsesSourceGen_ForArraysAndDictionaries` – arrays and dictionaries.
>   3. `JsonFilter_FluidValueWrapping_DoesNotBreakSourceGen` – direct `FluidValue` instances and mixed arrays.
>   4. `JsonFilter_RespectsCamelCaseNamingPolicy_FromSourceGen` – asserts naming policy.
> 
> ### Test Implementation Notes
> - Tests target existing TFMs (net8.0, net9.0) defined in `Fluid.Tests.csproj`.
> - No project file changes are strictly required; source generation attributes work with the in-box `System.Text.Json` for these TFMs.
> - Uses `TemplateOptions.MemberAccessStrategy.Register<T>()` for model types and `WithMiscFilters()` extension to ensure the `json` filter is available.
> - Ensures `Template.TryParse` succeeds and reports errors if parsing fails.
> - Attaches the generated context via:
>   ```csharp
>   var genOptions = new JsonSerializerOptions(FluidJsonContext.Default.Options);
>   genOptions.TypeInfoResolverChain.Add(FluidJsonContext.Default);
>   o.JsonSerializerOptions = genOptions;
>   ```
> 
> ### Non-Goals
> - Performance benchmarking.
> - Encoder variations (e.g., `UnsafeRelaxedJsonEscaping`).
> - Indented formatting or DateTime specific tests.
> 
> ### Future Enhancements (Optional)
> Additional tests could be added later for:
> - Custom `JavaScriptEncoder` behavior with source generation.
> - Indented output combined with source-gen context.
> - Large object graphs for performance validation.
> - DateTime / DateTimeOffset and custom converters registered alongside source‑gen.
> 
> ### File Content
> ```csharp
> using System;
> using System.Collections.Generic;
> using System.Text.Json;
> using System.Text.Json.Serialization;
> using Fluid;
> using Fluid.Values;
> using Xunit;
> 
> namespace Fluid.Tests
> {
>     [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
>     [JsonSerializable(typeof(Person))]
>     [JsonSerializable(typeof(Address))]
>     [JsonSerializable(typeof(List<string>))]
>     [JsonSerializable(typeof(Dictionary<string, int>))]
>     [JsonSerializable(typeof(string[]))]
>     public partial class FluidJsonContext : JsonSerializerContext { }
> 
>     public sealed class Person
>     {
>         public string FirstName { get; init; } = string.Empty;
>         public string LastName { get; init; } = string.Empty;
>         public int Age { get; init; }
>         public Address HomeAddress { get; init; }
>         public List<string> Tags { get; init; }
>         public Dictionary<string, int> Scores { get; init; }
>     }
> 
>     public sealed class Address
>     {
>         public string Street { get; init; } = string.Empty;
>         public string City { get; init; } = string.Empty;
>     }
> 
>     public class JsonSourceGenTests
>     {
>         private static Template Parse(string liquid)
>         {
>             Assert.True(Template.TryParse(liquid, out var template, out var errors), string.Join(Environment.NewLine, errors));
>             return template;
>         }
> 
>         private static TemplateContext CreateContext(Action<TemplateOptions> configureOptions = null)
>         {
>             var options = new TemplateOptions();
>             options.MemberAccessStrategy = new DefaultMemberAccessStrategy();
>             options.MemberAccessStrategy.Register<Person>();
>             options.MemberAccessStrategy.Register<Address>();
>             configureOptions?.Invoke(options);
>             return new TemplateContext(options);
>         }
> 
>         [Fact]
>         public void JsonFilter_UsesSourceGeneratedContext_ForSimpleObject()
>         {
>             var person = new Person
>             {
>                 FirstName = "John",
>                 LastName = "Doe",
>                 Age = 42,
>                 HomeAddress = new Address { Street = "123 Main", City = "Metropolis" },
>                 Tags = new List<string> { "admin", "author" },
>                 Scores = new Dictionary<string, int> { ["math"] = 95, ["science"] = 90 }
>             };
> 
>             var ctx = CreateContext(o =>
>             {
>                 var genOptions = new JsonSerializerOptions(FluidJsonContext.Default.Options);
>                 genOptions.TypeInfoResolverChain.Add(FluidJsonContext.Default);
>                 o.JsonSerializerOptions = genOptions;
>             });
> 
>             ctx.Options.Filters.WithMiscFilters();
>             ctx.SetValue("person", person);
>             var template = Parse("{{ person | json }}");
>             var rendered = template.Render(ctx);
>             var direct = JsonSerializer.Serialize(person, FluidJsonContext.Default.Person);
>             Assert.Equal(direct, rendered);
>         }
> 
>         [Fact]
>         public void JsonFilter_UsesSourceGen_ForArraysAndDictionaries()
>         {
>             var ctx = CreateContext(o =>
>             {
>                 var genOptions = new JsonSerializerOptions(FluidJsonContext.Default.Options);
>                 genOptions.TypeInfoResolverChain.Add(FluidJsonContext.Default);
>                 o.JsonSerializerOptions = genOptions;
>             });
> 
>             ctx.Options.Filters.WithMiscFilters();
>             var tags = new[] { "one", "two", "three" };
>             var scores = new Dictionary<string, int> { ["alpha"] = 1, ["beta"] = 2 };
>             ctx.SetValue("tags", tags);
>             ctx.SetValue("scores", scores);
>             var template = Parse("Tags: {{ tags | json }} Scores: {{ scores | json }}");
>             var rendered = template.Render(ctx);
>             var directTags = JsonSerializer.Serialize(tags, FluidJsonContext.Default.StringArray);
>             var directScores = JsonSerializer.Serialize(scores, FluidJsonContext.Default.DictionaryStringInt32);
>             Assert.Equal($"Tags: {directTags} Scores: {directScores}", rendered);
>         }
> 
>         [Fact]
>         public void JsonFilter_FluidValueWrapping_DoesNotBreakSourceGen()
>         {
>             var ctx = CreateContext(o =>
>             {
>                 var genOptions = new JsonSerializerOptions(FluidJsonContext.Default.Options);
>                 genOptions.TypeInfoResolverChain.Add(FluidJsonContext.Default);
>                 o.JsonSerializerOptions = genOptions;
>             });
> 
>             ctx.Options.Filters.WithMiscFilters();
>             var fluidString = new StringValue("Hello SourceGen");
>             ctx.SetValue("msg", fluidString);
>             var array = new object[] { "A", 123, true };
>             ctx.SetValue("mixed", array);
>             var template = Parse("{{ msg | json }} {{ mixed | json }}");
>             var rendered = template.Render(ctx);
>             var expectedMsg = JsonSerializer.Serialize("Hello SourceGen", FluidJsonContext.Default.String);
>             var expectedMixed = JsonSerializer.Serialize(array, ctx.JsonSerializerOptions);
>             Assert.Equal($"{expectedMsg} {expectedMixed}", rendered);
>         }
> 
>         [Fact]
>         public void JsonFilter_RespectsCamelCaseNamingPolicy_FromSourceGen()
>         {
>             var person = new Person
>             {
>                 FirstName = "Jane",
>                 LastName = "Roe",
>                 Age = 30,
>                 HomeAddress = new Address { Street = "500 Market", City = "Gotham" },
>                 Tags = new List<string> { "x", "y" },
>                 Scores = new Dictionary<string, int> { ["logic"] = 88 }
>             };
> 
>             var ctx = CreateContext(o =>
>             {
>                 var genOptions = new JsonSerializerOptions(FluidJsonContext.Default.Options);
>                 genOptions.TypeInfoResolverChain.Add(FluidJsonContext.Default);
>                 o.JsonSerializerOptions = genOptions;
>             });
> 
>             ctx.Options.Filters.WithMiscFilters();
>             ctx.SetValue("person", person);
>             var template = Parse("{{ person | json }}");
>             var output = template.Render(ctx);
>             Assert.Contains("\"firstName\":\"Jane\"", output);
>             Assert.Contains("\"homeAddress\":{\"street\":\"500 Market\",\"city\":\"Gotham\"}", output);
>         }
>     }
> }
> ```

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey).

Copilot AI changed the title [WIP] Add test suite for Fluid's json filter compatibility Add test coverage for System.Text.Json source generation compatibility Nov 11, 2025
Copilot AI requested a review from sebastienros November 11, 2025 09:39
Copilot finished work on behalf of sebastienros November 11, 2025 09:39
@sebastienros sebastienros marked this pull request as ready for review November 11, 2025 09:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants