dotnet / dotnet/runtime

[API Proposal]: Custom converters for external types in `System.Text.Json` source generation

Open
#130,331 4 comments 1 reaction 0 assignees View on GitHub
api-suggestion area-System.Text.Json
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.