[API Proposal]: Custom converters for external types in `System.Text.Json` source generation
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Description
### Background and motivation
Allow specifying custom converters for external types for `System.Text.Json` source generation.
`JsonSourceGenerationOptions(Converters = [...])` is not equivalent to placing `[JsonConverter]` on the target type for source-generation purposes:
- The converter participates through `Default.Options.Converters` so generated metadata can pick it up at runtime
- However, the source generator does not treat that as design-time converter metadata for the target type itself
External types run into the following problems:
- The user cannot add `[JsonConverter]` to the type
- Adding `[JsonConverter]` to all properties of that type is brittle and verbose
- the source generator continues crawling the type graph instead of stopping at the converter boundary. Depending on the type, that can generate a large amount of unnecessary metadata and code
### API Proposal
When a `JsonSerializerContext` declares an external converter mapping for type `T`, source generation should treat that mapping exactly as if `[JsonConverter(typeof(...))]` had been placed directly on `T`:
- the converter becomes part of generated `JsonTypeInfo`
- source generation stops inspecting `T` any further
- the existing design-time custom-converter pipeline is reused
- the same converter validation rules and diagnostics are reused where possible
- a context-level converter should override a direct `[JsonConverter]` on the target type
- if a converter is registered for value type `T`, that converter should also apply to `Nullable` unless a `Nullable` converter registration exists
Attribute declaration:
```csharp
namespace System.Text.Json.Serialization;
///
/// Specifies a JSON converter for JSON source generation
///
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class JsonExternalConverterAttribute : JsonAttribute
{
public Type ConverterType { get; }
public JsonExternalConverterAttribute(Type converterType) => ConverterType = converterType;
}
```
### API Usage
```csharp
public class CustomModel
{
public TimeZoneInfo? Timezone { get; set; }
}
public class TimezoneJsonConverter : JsonConverter
{
public override TimeZoneInfo Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var timezoneId = reader.GetString()!;
return TimeZoneInfo.FindSystemTimeZoneById(timezoneId) ?? throw new Exception($"Timezone {timezoneId} not found");
}
public override void Write(Utf8JsonWriter writer, TimeZoneInfo value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.Id);
}
}
[JsonSerializable(typeof(CustomModel))]
[JsonExternalConverter(typeof(TimezoneJsonConverter))]
public partial class ApiJsonContext : StrongIdJsonSerializerContext;
```
In this example, `TimeZoneInfo` should be handled exactly as though `TimeZoneInfo` itself (or the `Timezone` property) has been annotated with `[JsonConverter(typeof(TimezoneJsonConverter))]`.
### Alternative Designs
_No response_
### Risks
_No response_
Contributor guide
Research direction
Start by reviewing the existing design-time custom-converter pipeline and the JsonSerializerContext and JsonSourceGenerationOptions entry points. Evaluate the proposed JsonExternalConverterAttribute against existing converter validation and diagnostics. Done means external type mappings affect generated JsonTypeInfo, stop source-generation traversal, and follow the stated precedence and nullable behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- api, devtools
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100