Skip to content

Allow custom JsonSerializerOptions (and converters) in JsonLogMessageFormatter #2350

@madmox

Description

@madmox

Describe the feature

The JsonLogMessageFormatter introduced in Amazon.Lambda.RuntimeSupport 1.12.0 hardcodes its JsonSerializerOptions in the parameterless constructor:

// JsonLogMessageFormatter.cs
private readonly JsonSerializerOptions _jsonSerializationOptions;

public JsonLogMessageFormatter()
{
    _jsonSerializationOptions = new JsonSerializerOptions
    {
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
        WriteIndented = false
    };
}

There is currently no way for a customer to inject custom JsonSerializerOptions or register custom JsonConverters. The only documented customization path is to apply [JsonConverter(...)] attributes individually on every property of every type that may end up in a {@param} destructured log call.

Use Case

Many .NET codebases use third-party value types that have no public properties (so JsonSerializer.Serialize without converters produces empty {}) but provide official JsonConverters in companion packages. Examples:

  • NodaTime (Instant, LocalDate, LocalDateTime, Duration, Period...) - converters in NodaTime.Serialization.SystemTextJson
  • NetTopologySuite geographic types - converters in NetTopologySuite.IO.GeoJSON4STJ
  • Domain primitive / value object libraries like Vogen, StronglyTypedId - generated converters

In our codebase, NodaTime is used pervasively across business entities and SQS messages. Whenever we want to use the new {@Object} destructuring syntax to log a complex object, NodaTime properties serialize as empty objects, which silently loses data:

// Current output for `_logger.LogInformation("Email queued {@Message}", sqsMessage)`:
{
  "timestamp": "2026-04-29T10:00:00.000Z",
  "level": "Information",
  "requestId": "...",
  "message": "Email queued {@Message}",
  "Message": {
    "To": "alice@example.com",
    "EventDate": {},               // ← lost: should be "2026-04-29T10:00:00Z"
    "TemplateKey": "ACCOUNT_CREATED"
  }
}

Annotating each property of every entity with a custom [JsonConverter] attribute is invasive, mixes serialization concerns with persistence/domain code, and is fragile (easy to forget on a new property - silent data loss).

Proposed Solution

Expose a constructor overload (or a factory hook) that accepts a custom JsonSerializerOptions instance. Three possible levels of API, in order of preference:

  1. Minimal: add a static JsonLogMessageFormatter.ConfigureOptions(Action<JsonSerializerOptions>) method that the runtime applies after creating its default options. Optionally also support a declarative form via an AWS_LAMBDA_LOG_FORMATTER_OPTIONS_CONFIGURATOR environment variable pointing to a Type::Method, mirroring how [LambdaSerializer] attribute already lets users plug a custom serializer.
  2. DI-based: allow consumers to register a JsonSerializerOptions (or an IJsonSerializerOptionsProvider) in DI; the runtime support reads it on first log call.
  3. Subclass-friendly: change _jsonSerializationOptions to protected, add a virtual CreateJsonSerializerOptions() method, allow subclassing JsonLogMessageFormatter and selecting it via env var or DI.

Option 1 is the least invasive and matches how Amazon.Lambda.Serialization.SystemTextJson.SourceGeneratorLambdaJsonSerializer<T> already lets customers swap the serializer.

Other Information

Alternatives considered

  • Annotating every domain property with [JsonConverter]: works but invasive, fragile, mixes concerns.
  • Avoiding {@Object} on objects containing custom types: works but defeats much of the structured-logging value.
  • Pre-serializing in user code with JsonSerializer.Serialize(value, customOptions) then logging as a string: loses the "queryable hierarchical fields" benefit on the CloudWatch Logs Insights side.
  • Switching to a third-party logger (Serilog, NLog): works but requires customers to leave the AWS-recommended path and re-implement requestId/traceId enrichment.

Additional context

Adding this hook would close a real gap for any codebase using third-party value types with companion JsonConverters, without affecting the default behavior.

Acknowledgements

  • I may be able to implement this feature request
  • This feature might incur a breaking change

AWS .NET SDK and/or Package version used

  • Amazon.Lambda.RuntimeSupport: 1.12.0+ (provided by the managed runtime; source inspected on master branch as of issue submission)
  • Amazon.Lambda.Core: 2.4.0+
  • Amazon.Lambda.Logging.AspNetCore: 5.0.0+

Targeted .NET Platform

.NET 10 (net10.0), running on the AWS Lambda managed dotnet10 runtime. The same limitation applies on net8.0 / dotnet8.

Operating System and version

AWS Lambda managed runtime (Amazon Linux 2023, x86_64). Reproducible identically on arm64. Local repro on Windows 11 / .NET 10 SDK.

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature-requestA feature should be added or improved.p2This is a standard priority issuequeued

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions