镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Allow custom JsonSerializerOptions (and converters) in JsonLogMessageFormatter #2350

Open

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.

Activity

  1. added
    feature-requestA feature should be added or improved.
    needs-triageThis issue or PR still needs to be triaged.
    on Apr 29, 2026
  2. GarrettBeatty commented on Apr 30, 2026

    @GarrettBeatty
    Contributor

    Thanks for opening the issue. Feel free to contribute PR that can be reviewed by the team.

  3. added
    p2This is a standard priority issue
    and removed
    needs-triageThis issue or PR still needs to be triaged.
    on Apr 30, 2026
  4. logical-intent commented on May 10, 2026

    @logical-intent

    This issue also affects AOT-compiled lambda projects.

    If the logger would or could use the SourceGeneratorLambdaJsonSerializer that we pass to LambdaBootstrapBuilder.Create(...), it would be able to support structured logging of custom classes by the simple addition of one JsonSerializable attribute (per custom class).

    Today with the existing code, only the type name is printed.

    The only workaround I found that flows through to Logs Insights in a usable way is to bypass the logging system entirely and use a Console.WriteLine(JsonSerializer.Serialize(myCustomTypeObject, MyLambdaSerializerContext.Default.MyCustomType)) and switch the lambda logging from "JSON" to "Text" mode. It's a kludge but I'll take it. (With a side-note that the currently published Powertools AOT logging example produces trim warnings with .NET 10, so that's not a viable alternative workaround either.)

  5. normj commented on May 13, 2026

    @normj
    Member

    Let met take a crack at adding a ConfigureStructuredLogger method to Amazon.Lambda.Core.LambdaLogger that would allow you passing in your own instance of JsonSerializerOptions.

  6. madmox commented on May 13, 2026

    @madmox
    Author

    Thanks for opening the issue. Feel free to contribute PR that can be reviewed by the team.

    I would have loved to contribute a PR on this, but after looking more closely at the existing codebase I realize I lack the broader perspective needed to propose a solid implementation that covers all possible use cases. I wouldn't want to introduce a half-baked solution that misses edge cases or conflicts with the existing serialization pipeline.

    I'll leave it to the maintainers or someone with deeper familiarity with the internals. Happy to help test or provide feedback on any proposed approach though.

  7. normj commented on May 13, 2026

    @normj
    Member

    Work between Amazon.Lambda.Core and Amazon.Lambda.RuntimeSupport is tricky because Amazon.Lambda.RuntimeSupport has a dependency on Amazon.Lambda.Core but since Amazon.Lambda.Core is brought in via the deployment bundle and Amazon.Lambda.RuntimeSupport is built into the managed runtime you can't guarantee you will be working with the version of Amazon.Lambda.Core you expect in Amazon.Lambda.RuntimeSupport. So you have to be extra defensive for missing types and methods. It is a pain and one we can't easily rollback if we make mistakes. I appreciate the effort but I think in this case it is best if I take lead on the code.

    I'll ping when I have the PR out so you can give feedback on the public API and when the PR is done it will go out as a preview. In preview you can test using the executable programming model. I would appreciate any feedback during preview when I could still make changes.

  8. normj commented on May 18, 2026

    @normj
    Member

    PR #2383 is out to allow customizing the JsonSerializerOptions used for serializing the message parameters.

  9. normj commented on May 18, 2026

    @normj
    Member

    Version 3.1.0 of Amazon.Lambda.Core is out which has the LambdaLogger.ConfigureStructuredLogging API which is currently in preview till the managed runtime gets updated. Till then you can use the new API when you deploy as a executable and include the latest versions of Amazon.Lambda.Core and Amazon.Lambda.RuntimeSupport.

  10. madmox commented on Sep 1, 2026

    @madmox
    Author

    Feedback on the deployed state of ConfigureStructuredLogging, since the 2026-07-29 release notes say it "has been deployed to the managed runtime": it does not appear to be effective on the managed runtime yet, at least for class-library functions.

    Setup: .NET 10 class-library function on the managed dotnet10 runtime, eu-west-3, runtime version dotnet:10.mainline.v43 (ARN ...:runtime:fe3aeb65c42423af6d5d92aa7c69504ad9216c72e3d6529ce5db3931f8e1a61d), Amazon.Lambda.Core 3.3.0 in the deployment bundle, JSON log format. At startup we call LambdaLogger.ConfigureStructuredLogging with JsonSerializerOptions carrying the NodaTime converters.

    Observed in CloudWatch: a destructured {@ScheduledDate} (NodaTime.Instant) still renders as "ScheduledDate":{}. The override never reaches the formatter. The rendered message text shows the correct value (ToString() path), and plain POCOs destructure fine, so only the converter registration is missing.

    Control experiment: the exact same call against the Amazon.Lambda.RuntimeSupport 2.2.0 NuGet package (instantiating JsonLogMessageFormatter directly) works: the override is applied and the Instant serializes as "2026-09-01T15:40:00Z". So the application side is fine; the RuntimeSupport build embedded in the managed runtime seems to predate the feature, or the class-library path doesn't wire the callback (the preview-phase note said testing was possible with the executable model).

    Happy to run any diagnostic you'd find useful on our side.

  11. normj commented on Sep 24, 2026

    @normj
    Member

    @madmox I have reproduced your issue and looking into what is going on.

  12. github-actions commented on Sep 25, 2026

    @github-actions
    Contributor

    Comments on closed issues are hard for our team to see.
    If you need more assistance, please either tag a team member or open a new issue that references this one.
    If you wish to keep having a conversation with other community members under this issue feel free to do so.

  13. reopened this on Sep 25, 2026
  14. normj commented on Sep 25, 2026

    @normj
    Member

    @madmox Sorry didn't mean to close the issue.

    The fix has been released to NuGet which won't help the managed runtime but I have also pushed the change over to the Lambda team who will pick up the fix as part of the next deployment. Let's leave the issue open till we have confirmed the fix in a deployment managed runtime.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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