Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions aws-lambda-java-runtime-interface-client/RELEASE.CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
### September 30, 2026
`2.13.0`
- **Behavior change:** POJO handler input and output are now always serialized with the runtime's Jackson serializer. Previously, invocations whose client context reported `env.platform` as `Android` (as set by the archived AWS Mobile SDK for Android `LambdaInvokerFactory`) were serialized with Gson, which binds fields directly instead of bean properties. Invocations without that client context are unaffected. Android callers now receive the same JSON as every other caller, which differs from before as follows:
- Private fields without getters or setters are no longer read from the request or written to the response. Add getters and setters, or make the fields public.
- Response keys follow getter names instead of field names: a field `isActive` with `isActive()` becomes `active`, and a field `mName` with `getName()` becomes `name`. Name the accessors after the JSON key (`getIsActive`), or use a public field with that name.
- Getters without a backing field, and `transient` fields that have getters, add keys to the response. Rename methods that should not be serialized so they are not getters.
- Fields with a getter but no setter keep their default value instead of the request value. Add a setter.
- Types without a no-argument constructor fail the invocation with `An error occurred during JSON parsing`. Add a no-argument constructor and setters.
- Numbers in `Object` or `Map<String, Object>` fields are read as `Integer`, `Long` or `Double` instead of always `Double`, so `1` is no longer echoed as `1.0`.
- `java.util.Date` values are written as numbers instead of formatted text.
- All of the above also apply to nested objects and list elements.

Each case and its fix is covered in [`GsonToJacksonPojoSerializationTest`](https://github.com/aws/aws-lambda-java-libs/blob/main/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/GsonToJacksonPojoSerializationTest.java). If changing the types is not possible, register a [`CustomPojoSerializer`](https://github.com/aws/aws-lambda-java-libs/tree/main/samples/custom-serialization/gson); note that a custom serializer applies to every invocation of the function, not only to Android callers.

### September 2, 2026
`2.12.1`
- Emit a structured `runtime_worker_pool_initializing` DEBUG log event once during INIT in multi-concurrent (Lambda Managed Instances) mode, reporting the worker pool size (`workerCount`) and the maximum concurrency the execution environment supports (`executionEnvironmentMaxConcurrency`). Only visible when the function log level is DEBUG or lower; not emitted for standard on-demand functions.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

package com.amazonaws.services.lambda.runtime.api.client;

import com.amazonaws.services.lambda.runtime.ClientContext;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.LambdaLogger;
import com.amazonaws.services.lambda.runtime.LambdaRuntimeInternal;
Expand Down Expand Up @@ -38,11 +37,9 @@
import java.lang.reflect.TypeVariable;
import java.util.Arrays;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import static com.amazonaws.services.lambda.runtime.api.client.UserFault.filterStackTrace;
import static com.amazonaws.services.lambda.runtime.api.client.UserFault.makeUserFault;
Expand All @@ -51,16 +48,10 @@
public final class EventHandlerLoader {
private static final byte[] _JsonNull = new byte[]{'n', 'u', 'l', 'l'};

private enum Platform {
ANDROID,
IOS,
UNKNOWN
}

private static volatile ThreadLocal<PojoSerializer<LambdaClientContext>> contextSerializer = new ThreadLocal<>();
private static volatile ThreadLocal<PojoSerializer<LambdaCognitoIdentity>> cognitoSerializer = new ThreadLocal<>();

private static final ThreadLocal<EnumMap<Platform, Map<Type, PojoSerializer<Object>>>> typeCache = ThreadLocal.withInitial(() -> new EnumMap<>(Platform.class));
private static final ThreadLocal<Map<Type, PojoSerializer<Object>>> typeCache = ThreadLocal.withInitial(HashMap::new);

private static final Comparator<Method> methodPriority = new Comparator<Method>() {
public int compare(Method lhs, Method rhs) {
Expand Down Expand Up @@ -97,16 +88,15 @@ private EventHandlerLoader() {
}

/**
* returns the appropriate serializer for the class based on platform and whether the class is a supported event
* returns the serializer for the type: the customer's CustomPojoSerializer if one is registered, the event
* serializer if the type is a supported Lambda event, and Jackson otherwise
*
* @param platform enum platform
* @param type Type of object used
* @param type Type of object used
* @return PojoSerializer
* @see Platform for which platforms are used
* @see LambdaEventSerializers for how mixins and modules are added to the serializer
*/
@SuppressWarnings({"unchecked", "rawtypes"})
private static PojoSerializer<Object> getSerializer(Platform platform, Type type) {
private static PojoSerializer<Object> getSerializer(Type type) {
PojoSerializer<Object> customSerializer = PojoSerializerLoader.getCustomerSerializer(type);
if (customSerializer != null) {
return customSerializer;
Expand All @@ -119,24 +109,15 @@ private static PojoSerializer<Object> getSerializer(Platform platform, Type type
return LambdaEventSerializers.serializerFor(clazz, AWSLambda.getCustomerClassLoader());
}
}
// else platform dependent (Android uses GSON but all other platforms use Jackson)
if (Objects.requireNonNull(platform) == Platform.ANDROID) {
return GsonFactory.getInstance().getSerializer(type);
}
return JacksonFactory.getInstance().getSerializer(type);
}

private static PojoSerializer<Object> getSerializerCached(Platform platform, Type type) {
EnumMap<Platform, Map<Type, PojoSerializer<Object>>> threadTypeCache = typeCache.get();
Map<Type, PojoSerializer<Object>> cache = threadTypeCache.get(platform);
if (cache == null) {
cache = new HashMap<>();
threadTypeCache.put(platform, cache);
}
private static PojoSerializer<Object> getSerializerCached(Type type) {
Map<Type, PojoSerializer<Object>> cache = typeCache.get();

PojoSerializer<Object> serializer = cache.get(type);
if (serializer == null) {
serializer = getSerializer(platform, type);
serializer = getSerializer(type);
cache.put(type, serializer);
}

Expand All @@ -158,31 +139,6 @@ private static PojoSerializer<LambdaCognitoIdentity> getCognitoSerializer() {
}


private static Platform getPlatform(Context context) {
ClientContext cc = context.getClientContext();
if (cc == null) {
return Platform.UNKNOWN;
}

Map<String, String> env = cc.getEnvironment();
if (env == null) {
return Platform.UNKNOWN;
}

String platform = env.get("platform");
if (platform == null) {
return Platform.UNKNOWN;
}

if ("Android".equalsIgnoreCase(platform)) {
return Platform.ANDROID;
} else if ("iPhoneOS".equalsIgnoreCase(platform)) {
return Platform.IOS;
} else {
return Platform.UNKNOWN;
}
}

private static boolean isVoid(Type type) {
return Void.TYPE.equals(type) || (type instanceof Class) && Void.class.isAssignableFrom((Class<?>) type);
}
Expand Down Expand Up @@ -629,11 +585,11 @@ public PojoHandlerAsStreamHandler(


if (inputType.isPresent()) {
getSerializerCached(Platform.UNKNOWN, inputType.get());
getSerializerCached(inputType.get());
}

if (outputType.isPresent()) {
getSerializerCached(Platform.UNKNOWN, outputType.get());
getSerializerCached(outputType.get());
}
}

Expand All @@ -642,10 +598,9 @@ public PojoHandlerAsStreamHandler(
public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context)
throws IOException {
final Object input;
final Platform platform = getPlatform(context);
try {
if (inputType.isPresent()) {
input = getSerializerCached(platform, inputType.get()).fromJson(inputStream);
input = getSerializerCached(inputType.get()).fromJson(inputStream);
} else {
input = null;
}
Expand All @@ -662,7 +617,7 @@ public void handleRequest(InputStream inputStream, OutputStream outputStream, Co

try {
if (outputType.isPresent()) {
PojoSerializer<Object> serializer = getSerializerCached(platform, outputType.get());
PojoSerializer<Object> serializer = getSerializerCached(outputType.get());
serializer.toJson(output, outputStream);
} else {
outputStream.write(_JsonNull);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
Expand All @@ -11,6 +13,7 @@
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
Expand Down Expand Up @@ -59,6 +62,46 @@ void PojoHandlerTest_twoParams() throws Exception {
assertSuccessfulInvocation(lambdaRequestHandler);
}

// Before 2.13.0, a client context with env.platform equal to "Android" (case-insensitive) switched POJO
// serialization to Gson, which binds fields: pojoOutputHandler returned {"internalField":"field-based-value"}
// and pojoInputHandler returned "x:true". Every client context must now produce the Jackson result.

static Stream<String> clientContexts() {
return Stream.of(
null,
"{\"env\":{\"platform\":\"Android\"}}",
"{\"env\":{\"platform\":\"android\"}}",
"{\"env\":{\"platform\":\"iPhoneOS\"}}",
"{\"env\":{}}",
"{}");
}

@ParameterizedTest(name = "clientContext={0}")
@MethodSource("clientContexts")
void PojoHandler_outputSerializer_ignoresClientContextPlatform(String clientContext) throws Exception {
LambdaRequestHandler handler =
getLambdaRequestHandler("test.lambda.handlers.POJOHanlderImpl::pojoOutputHandler");

InvocationRequest request = getTestInvocationRequest();
request.setClientContext(clientContext);

assertEquals("{\"beanProperty\":\"property-based-value\"}", handler.call(request).toString());
}

@ParameterizedTest(name = "clientContext={0}")
@MethodSource("clientContexts")
void PojoHandler_inputSerializer_ignoresClientContextPlatform(String clientContext) throws Exception {
LambdaRequestHandler handler =
getLambdaRequestHandler("test.lambda.handlers.POJOHanlderImpl::pojoInputHandler");

InvocationRequest request = getTestInvocationRequest();
request.setClientContext(clientContext);
request.setContent("{\"name\":\"x\",\"locked\":true}".getBytes());

// name has a setter and binds; locked has only a getter and keeps its default
assertEquals("\"x:false\"", handler.call(request).toString());
}

private LambdaRequestHandler getLambdaRequestHandler(String handler) throws ClassNotFoundException {
ClassLoader cl = this.getClass().getClassLoader();
HandlerInfo handlerInfo = HandlerInfo.fromString(handler, cl);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,41 @@ public String oneParamHandler_context(Context context) {
public String twoParamsHandler(String event, Context context) {
return "success";
}

@SuppressWarnings("unused")
public PojoOutput pojoOutputHandler(String event) {
return new PojoOutput();
}

@SuppressWarnings("unused")
public static class PojoOutput {
private final String internalField = "field-based-value";

public String getBeanProperty() {
return "property-based-value";
}
}

@SuppressWarnings("unused")
public String pojoInputHandler(PojoInput input) {
return input.getName() + ":" + input.isLocked();
}

@SuppressWarnings("unused")
public static class PojoInput {
private boolean locked = false;
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public boolean isLocked() {
return locked;
}
}
}
Loading
Loading