Java Serverless on AWS Lambda · Java on Lambda: Runtime Fundamentals
Handlers, Event Types and Serialization
Why a Java handler receives objects full of nulls, and how to model Lambda events correctly.
Scenario
Example app: a Java function consumes order events from EventBridge. The team wrote a POJO OrderEvent with fields orderId, totalAmount and customerEmail and uses it as the handler's input type.
What you see in production
The function runs without errors, but every field is null and orders are processed with empty data. When an upstream team adds a field, some invocations start failing with deserialization errors.
The naive code
public class OrderHandler implements RequestHandler<OrderEvent, String> {
@Override
public String handleRequest(OrderEvent event, Context context) {
process(event.getOrderId(), event.getTotalAmount()); // nulls: the JSON is nested and snake_case
return "ok";
}
}
// Incoming: {"detail-type":"OrderPlaced","detail":{"order_id":"o-1","total_amount":42.5}}
Reproduce it
A tiny mapper with the same behaviour as naive bean-style binding: it only fills fields whose names match the top-level JSON keys exactly:
import java.util.*;
public class Main {
record Order(String orderId, Double totalAmount) {}
static Order bindTopLevel(Map<String, Object> json) {
return new Order((String) json.get("orderId"), (Double) json.get("totalAmount"));
}
@SuppressWarnings("unchecked")
static Order bindDetail(Map<String, Object> json) {
Map<String, Object> detail = (Map<String, Object>) json.get("detail");
if (detail == null || detail.get("order_id") == null) throw new IllegalArgumentException("missing detail.order_id");
return new Order((String) detail.get("order_id"), ((Number) detail.get("total_amount")).doubleValue());
}
public static void main(String[] args) {
Map<String, Object> event = Map.of(
"detail-type", "OrderPlaced",
"detail", Map.of("order_id", "o-1", "total_amount", 42.5));
System.out.println("naive POJO binding: " + bindTopLevel(event));
System.out.println("envelope-aware: " + bindDetail(event));
}
}
Output
naive POJO binding: Order[orderId=null, totalAmount=null] envelope-aware: Order[orderId=o-1, totalAmount=42.5]
Run this on your computer — see “Get Started” for setup.
Root cause
Lambda's Java runtime deserializes the event JSON into your input type by matching property names. EventBridge, SQS, API Gateway and other sources wrap your payload in an envelope with their own structure and naming (detail, Records[].body, body as a string). A POJO that models only the business payload, or uses different naming, silently gets nulls. Strict mappers fail on unknown properties when publishers add fields.
The fix
- Use the official event types from
aws-lambda-java-events(ScheduledEvent/EventBridge events,SQSEvent,APIGatewayV2HTTPEvent) for the envelope. - Deserialize the payload yourself with a configured Jackson
ObjectMapper(created once, statically): snake_case naming,FAIL_ON_UNKNOWN_PROPERTIESdisabled, and validation of required fields. - Or take the raw stream (
RequestStreamHandler) for full control.
public class OrderHandler implements RequestHandler<SQSEvent, SQSBatchResponse> {
private static final ObjectMapper MAPPER = new ObjectMapper()
.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
@Override
public SQSBatchResponse handleRequest(SQSEvent event, Context context) {
List<SQSBatchResponse.BatchItemFailure> failures = new ArrayList<>();
for (SQSEvent.SQSMessage msg : event.getRecords()) {
try {
OrderPlaced order = MAPPER.readValue(msg.getBody(), OrderPlaced.class);
process(order.validate());
} catch (Exception e) {
failures.add(new SQSBatchResponse.BatchItemFailure(msg.getMessageId()));
}
}
return new SQSBatchResponse(failures);
}
}
Trade-offs
- Custom deserialization adds a little code but makes the contract explicit and testable.
- Ignoring unknown properties keeps consumers working when publishers add fields; validate the fields you rely on.
- Powertools for AWS Lambda (Java) offers helpers for deserializing common envelopes.
Detect it in production
- Unit tests with real sample events for each source (the
aws-lambda-java-testslibrary loads JSON events into the event types). - Alarms on processed records with missing required data; validation failure counts.
- Logs of deserialization errors after upstream deployments.
Key points
- Events arrive in source-specific envelopes; model them with
aws-lambda-java-events. - Deserialize payloads with a static, configured
ObjectMapperand validate. - Test with real sample events.
Further reading: Define Lambda function handlers in Java · aws-lambda-java-libs on GitHub
Practice questions
Why might a Java handler's POJO input have all fields null?
Answer
The runtime binds JSON properties to fields by name. Event sources wrap the payload in their own envelope (for example EventBridge's detail) and may use different naming, so a POJO modelling only the business payload matches nothing.
Where should a Jackson ObjectMapper be created in a Lambda function?
Answer
Once, in a static field or during initialisation, because it is expensive to create and thread-safe after configuration; creating it per invocation wastes time and memory.
What is aws-lambda-java-tests useful for?
Answer
Loading JSON sample events into the aws-lambda-java-events types in JUnit tests, so handlers can be tested with realistic payloads for each event source.
Open in the app to mark lessons complete, take notes and track your progress.