Skip to content

Feature request: Allow configuring an alternative JSON decoder for response deserialization #3374

@dghood1

Description

@dghood1

Describe the feature

Allow SDK users to provide a custom JSON decoder implementation for response deserialization, instead of the hardcoded encoding/json.NewDecoder used in generated deserializers.go files.

Currently, every service's generated deserializer creates a stdlib JSON decoder directly:

 // From generated deserializers.go (e.g., DynamoDB, S3, SQS)
 decoder := json.NewDecoder(body)
 decoder.UseNumber()
 var shape interface{}
 if err := decoder.Decode(&shape); err != nil && err != io.EOF {
     return out, metadata, &smithy.DeserializationError{...}
 }

This should be configurable via a client option so users can inject a faster, API-compatible JSON decoder.

Use Case

In high-throughput Go services, encoding/json response deserialization becomes a significant source of CPU time and heap allocations. Profiling a service making ~300 DynamoDB BatchWriteItem calls/sec shows
stdlib JSON decoding consuming ~8% of total heap allocations (425MB cumulative over a 60s pprof allocs profile).

Drop-in replacement libraries like github.com/segmentio/encoding/json offer substantially better performance while maintaining full API compatibility with encoding/json:

┌────────────────────┬────────────────────────┬─────────────────────────┬──────────────────────────────┐
│     Operation      │     encoding/json      │ segmentio/encoding/json │         Improvement          │
├────────────────────┼────────────────────────┼─────────────────────────┼──────────────────────────────┤
│ Unmarshal          │ 2,400 ns/op, 48 allocs │ 1,200 ns/op, 40 allocs  │ 50% faster, 17% fewer allocs │
├────────────────────┼────────────────────────┼─────────────────────────┼──────────────────────────────┤
│ Decode (streaming) │ similar ratio          │ similar ratio           │ ~50% faster                  │
└────────────────────┴────────────────────────┴─────────────────────────┴──────────────────────────────┘

We've already replaced encoding/json with segmentio/encoding/json throughout our own codebase and verified correctness + performance under load. However, we cannot extend this optimization to SDK internals
because the import is hardcoded in generated code.

The serialization side already uses the custom smithy-go/encoding/json encoder (not stdlib), so there's precedent for the SDK not relying on encoding/json in the hot path. The deserialization side is the
remaining gap.

Proposed Solution

Define a decoder interface in smithy-go and wire it through as a client option:

 // In smithy-go/encoding/json/ (or a new package)
 package json

 import "io"

 // Decoder creates JSON stream decoders from readers.
 type Decoder interface {
     NewDecoder(r io.Reader) StreamDecoder
 }

 // StreamDecoder reads and decodes JSON values from a stream.
 type StreamDecoder interface {
     UseNumber()
     Decode(v any) error
 }

 // StdlibDecoder is the default implementation wrapping encoding/json.
 type StdlibDecoder struct{}

 func (StdlibDecoder) NewDecoder(r io.Reader) StreamDecoder {
     return json.NewDecoder(r)
 }

Client option in generated service packages:

// In each service's options.go
client := dynamodb.NewFromConfig(cfg, func(o *dynamodb.Options) {
    o.JSONDecoder = myjson.NewDecoder() // custom implementation
})

Generated deserializers.go would change from:

 decoder := json.NewDecoder(body)
 decoder.UseNumber()

To:

 decoder := options.JSONDecoder.NewDecoder(body)
 decoder.UseNumber()

Where options.JSONDecoder defaults to StdlibDecoder{} if not configured.

Implementation would require:

  1. Interface definition in smithy-go runtime
  2. Codegen change in smithy-go-codegen to emit calls through the interface
  3. JSONDecoder field in generated Options structs with default
  4. Regeneration of service packages

I'm happy to contribute PRs if the team is open to this direction — would appreciate guidance on the preferred design before starting.

Other Information

Alternatives considered:

  • go.mod replace: Cannot replace stdlib packages via replace directive
  • Fork service packages: Would break on every SDK update — not maintainable
  • Accept the cost: Viable, but for high-throughput services the allocations are meaningful

Scope:

  • This affects all services using awsjson1.0 and awsjson1.1 protocols (DynamoDB, SQS, SNS, Lambda, etc.)
  • The same interface could optionally be extended to serialization, but the custom smithy-go encoder already performs well there

Compatible drop-in libraries:

  • github.com/segmentio/encoding/json — implements full encoding/json API surface
  • github.com/goccy/go-json — same
  • github.com/bytedance/sonic — same (with JIT on amd64)

Acknowledgements

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

AWS Go SDK V2 Module Versions Used

github.com/aws/aws-sdk-go-v2 v1.41.1
github.com/aws/aws-sdk-go-v2/config v1.32.7
github.com/aws/aws-sdk-go-v2/credentials v1.19.7
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.20.29
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.54.0
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.32.9
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.17
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17
github.com/aws/aws-sdk-go-v2/service/sqs v1.42.20
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6
github.com/aws/smithy-go v1.24.0

Go version used

1.26.1

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature-requestA feature should be added or improved.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions