aws / aws/aws-lambda-dotnet

Allow custom `JsonSerializerOptions` (and converters) in `JsonLogMessageFormatter`

Open
#2,350 8 comments 0 reactions 0 assignees View on GitHub
feature-request p2 queued
Dominant language
C#
Stars
1.7k
Forks
503
Avg merge
1d 5h
Merged PRs (30d)
18

Description

### Describe the feature

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

```csharp
// 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 `JsonConverter`s. 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 `JsonConverter`s 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:

```jsonc
// 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)` 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` 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

- Affected source file: [`Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/JsonLogMessageFormatter.cs`](https://github.com/aws/aws-lambda-dotnet/blob/master/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/JsonLogMessageFormatter.cs)
- Related issue: #1747 (closed, parent feature for structured logging)
- Documentation: [AWS .NET dev blog post on structured logging](https://aws.amazon.com/blogs/developer/structured-logging-for-net-lambda/) doesn't mention this customization path.

Adding this hook would close a real gap for any codebase using third-party value types with companion `JsonConverter`s, 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.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.