Table of Contents

Class ResultRequiresExplicitHttpMappingConverter

Namespace
Trellis
Assembly
Trellis.Core.dll

Default JsonConverterFactory attached to Result<TValue>, IResult, and IResult<TValue> that fails fast when a result is JSON-serialized without going through Trellis's HTTP-mapping path (Result<T>.ToHttpResponse() in Trellis.Asp) or an explicit consumer converter. Without this converter, direct serialization could only emit public status/error metadata such as {"IsSuccess": bool, "IsFailure": bool, "Error": …} — a struct dump with no success value, not a stable wire shape, and not the HTTP problem-details / status-code mapping consumers expect from Trellis.

public sealed class ResultRequiresExplicitHttpMappingConverter : JsonConverterFactory
Inheritance
ResultRequiresExplicitHttpMappingConverter
Inherited Members
Extension Methods

Remarks

The factory matches Result<TValue> AND the public result interfaces (IResult, IResult<TValue>). The attribute lives on both the struct and the interfaces because [JsonConverter] resolution is by static declared type — a controller signature like Task<IResult<int>> GetAsync() picks up the interface attribute, not the struct attribute (the runtime Result<int> is never consulted for converter resolution).

The intended controller / minimal-API pattern is to call .ToHttpResponse() on the result, which returns a Microsoft.AspNetCore.Http.IResult that writes the response body itself (so the Result<TValue> struct is never serialized by STJ). Returning a raw Result<TValue> from a controller action — and letting MVC's default object formatter pick it up — used to silently produce the struct-dump JSON shape with no HTTP status-code mapping for Error cases. With this converter attached, that misuse throws NotSupportedException on the first request with an actionable message naming .ToHttpResponse().

AOT-safe. The factory throws directly from CreateConverter(Type, JsonSerializerOptions) rather than instantiating a typed throwing converter via MakeGenericType / Activator.CreateInstance. This means no reflection-dependent paths run in Native AOT — consumers get the actionable Trellis message instead of an AOT-generated "native code not available" error before the message can fire.

Consumers who legitimately want to serialize a result for logging, IPC, or storage can register a converter (or another JsonConverterFactory) in Converters — option-registered converters take precedence over the type's JsonConverterAttribute. The override must match the declared static type of the value being serialized:

  • Value declared as Result<TValue> → register JsonConverter<Result<T>>.
  • Value declared as IResult<TValue> → register JsonConverter<IResult<T>>.
  • Value declared as IResult → register JsonConverter<IResult>.
  • Mixed shapes → register a JsonConverterFactory whose CanConvert returns true for every shape the consumer needs.

STJ resolves [JsonConverter] against the static declared type, not the runtime type, so a JsonConverter<Result<T>> alone does not cover IResult<T>-declared properties / return signatures.

Methods

CanConvert(Type)

Returns true for Result<TValue> and IResult<TValue> shapes, and for the non-generic IResult interface.

public override bool CanConvert(Type typeToConvert)

Parameters

typeToConvert Type

Returns

bool

CreateConverter(Type, JsonSerializerOptions)

Always throws NotSupportedException with the actionable Trellis message.

public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)

Parameters

typeToConvert Type
options JsonSerializerOptions

Returns

JsonConverter

Remarks

Throwing directly from CreateConverter(Type, JsonSerializerOptions) sidesteps the typed-converter instantiation entirely. The previous design used MakeGenericType + Activator.CreateInstance to materialise a typed JsonConverter<Result<T>> that would throw on its first Read / Write call — but those reflection paths are AOT-unsafe (IL2055 / IL3050) and could fail with a "native code not available" error before the Trellis message had a chance to surface. Throwing here keeps the converter AOT-safe and ensures consumers see the actionable Trellis message in every runtime configuration.