From 39456ce6053f2309438b868338911c8f59ef7854 Mon Sep 17 00:00:00 2001 From: RanVaknin <50976344+RanVaknin@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:14:16 -0700 Subject: [PATCH 1/5] Skip null non required fields in JsonProtocolMarshaller.doMarshall --- .../json/internal/marshall/JsonProtocolMarshaller.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/marshall/JsonProtocolMarshaller.java b/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/marshall/JsonProtocolMarshaller.java index eaa6c2ef5cd5..57db62d81e73 100644 --- a/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/marshall/JsonProtocolMarshaller.java +++ b/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/marshall/JsonProtocolMarshaller.java @@ -35,6 +35,7 @@ import software.amazon.awssdk.core.protocol.MarshallLocation; import software.amazon.awssdk.core.protocol.MarshallingType; import software.amazon.awssdk.core.traits.PayloadTrait; +import software.amazon.awssdk.core.traits.RequiredTrait; import software.amazon.awssdk.core.traits.TimestampFormatTrait; import software.amazon.awssdk.core.traits.TraitType; import software.amazon.awssdk.http.SdkHttpFullRequest; @@ -212,8 +213,13 @@ void doMarshall(SdkPojo pojo) { } } else if (isExplicitPayloadMember(field)) { marshallExplicitJsonPayload(field, val); - } else { + } else if (val != null) { marshallField(field, val); + } else if (field.containsTrait(RequiredTrait.class, + TraitType.REQUIRED_TRAIT)) { + throw new IllegalArgumentException( + String.format("Parameter '%s' must not be null", + field.locationName())); } } } From 1626e1b4ce2620628089716b0077d58c4449d89d Mon Sep 17 00:00:00 2001 From: RanVaknin <50976344+RanVaknin@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:29:46 -0700 Subject: [PATCH 2/5] Fix domarshall logic --- .../internal/marshall/JsonProtocolMarshaller.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/marshall/JsonProtocolMarshaller.java b/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/marshall/JsonProtocolMarshaller.java index 57db62d81e73..7bf7738a8563 100644 --- a/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/marshall/JsonProtocolMarshaller.java +++ b/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/marshall/JsonProtocolMarshaller.java @@ -215,11 +215,14 @@ void doMarshall(SdkPojo pojo) { marshallExplicitJsonPayload(field, val); } else if (val != null) { marshallField(field, val); - } else if (field.containsTrait(RequiredTrait.class, - TraitType.REQUIRED_TRAIT)) { - throw new IllegalArgumentException( - String.format("Parameter '%s' must not be null", - field.locationName())); + } else if (field.location() == MarshallLocation.PAYLOAD) { + if (field.containsTrait(RequiredTrait.class, TraitType.REQUIRED_TRAIT)) { + throw new IllegalArgumentException( + String.format("Parameter '%s' must not be null", + field.locationName())); + } + } else { + marshallField(field, val); } } } From f9e4340b2546405d6f9c9dfeba4c73af5c153b40 Mon Sep 17 00:00:00 2001 From: RanVaknin <50976344+RanVaknin@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:42:27 -0700 Subject: [PATCH 3/5] Fix logic, add null suppression --- .../amazon/awssdk/spotbugs-suppressions.xml | 8 ++++++++ .../internal/marshall/JsonProtocolMarshaller.java | 13 ++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml b/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml index 6871e760a793..16c9254404a1 100644 --- a/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml +++ b/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml @@ -523,4 +523,12 @@ + + + + + + + diff --git a/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/marshall/JsonProtocolMarshaller.java b/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/marshall/JsonProtocolMarshaller.java index 7bf7738a8563..427ae3779b13 100644 --- a/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/marshall/JsonProtocolMarshaller.java +++ b/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/marshall/JsonProtocolMarshaller.java @@ -215,14 +215,13 @@ void doMarshall(SdkPojo pojo) { marshallExplicitJsonPayload(field, val); } else if (val != null) { marshallField(field, val); - } else if (field.location() == MarshallLocation.PAYLOAD) { - if (field.containsTrait(RequiredTrait.class, TraitType.REQUIRED_TRAIT)) { - throw new IllegalArgumentException( - String.format("Parameter '%s' must not be null", - field.locationName())); - } - } else { + } else if (field.location() != MarshallLocation.PAYLOAD) { marshallField(field, val); + } else if (field.containsTrait(RequiredTrait.class, + TraitType.REQUIRED_TRAIT)) { + throw new IllegalArgumentException( + String.format("Parameter '%s' must not be null", + field.locationName())); } } } From 01e27392d29e1510df90b305b86864b0b33ce137 Mon Sep 17 00:00:00 2001 From: RanVaknin <50976344+RanVaknin@users.noreply.github.com> Date: Sun, 22 Mar 2026 16:50:11 -0700 Subject: [PATCH 4/5] Add test coverage --- .../amazon/awssdk/spotbugs-suppressions.xml | 2 +- .../marshall/JsonProtocolMarshaller.java | 6 +- .../marshall/JsonProtocolMarshallerTest.java | 197 ++++++++++++++++++ 3 files changed, 200 insertions(+), 5 deletions(-) create mode 100644 core/protocols/aws-json-protocol/src/test/java/software/amazon/awssdk/protocols/json/internal/marshall/JsonProtocolMarshallerTest.java diff --git a/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml b/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml index 16c9254404a1..69a7894bfe94 100644 --- a/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml +++ b/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml @@ -525,7 +525,7 @@ + whose NULL marshallers handle null validation. --> diff --git a/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/marshall/JsonProtocolMarshaller.java b/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/marshall/JsonProtocolMarshaller.java index 427ae3779b13..89ad22e6a54c 100644 --- a/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/marshall/JsonProtocolMarshaller.java +++ b/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/marshall/JsonProtocolMarshaller.java @@ -217,11 +217,9 @@ void doMarshall(SdkPojo pojo) { marshallField(field, val); } else if (field.location() != MarshallLocation.PAYLOAD) { marshallField(field, val); - } else if (field.containsTrait(RequiredTrait.class, - TraitType.REQUIRED_TRAIT)) { + } else if (field.containsTrait(RequiredTrait.class, TraitType.REQUIRED_TRAIT)) { throw new IllegalArgumentException( - String.format("Parameter '%s' must not be null", - field.locationName())); + String.format("Parameter '%s' must not be null", field.locationName())); } } } diff --git a/core/protocols/aws-json-protocol/src/test/java/software/amazon/awssdk/protocols/json/internal/marshall/JsonProtocolMarshallerTest.java b/core/protocols/aws-json-protocol/src/test/java/software/amazon/awssdk/protocols/json/internal/marshall/JsonProtocolMarshallerTest.java new file mode 100644 index 000000000000..58190503beca --- /dev/null +++ b/core/protocols/aws-json-protocol/src/test/java/software/amazon/awssdk/protocols/json/internal/marshall/JsonProtocolMarshallerTest.java @@ -0,0 +1,197 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.protocols.json.internal.marshall; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.SdkField; +import software.amazon.awssdk.core.SdkPojo; +import software.amazon.awssdk.core.protocol.MarshallLocation; +import software.amazon.awssdk.core.protocol.MarshallingType; +import software.amazon.awssdk.core.traits.LocationTrait; +import software.amazon.awssdk.core.traits.RequiredTrait; +import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.protocols.core.OperationInfo; +import software.amazon.awssdk.protocols.core.ProtocolMarshaller; +import software.amazon.awssdk.protocols.json.AwsJsonProtocol; +import software.amazon.awssdk.protocols.json.AwsJsonProtocolMetadata; +import software.amazon.awssdk.protocols.json.internal.AwsStructuredPlainJsonFactory; + +class JsonProtocolMarshallerTest { + + private static final URI ENDPOINT = URI.create("http://localhost"); + private static final String CONTENT_TYPE = "application/x-amz-json-1.0"; + private static final OperationInfo OP_INFO = OperationInfo.builder() + .httpMethod(SdkHttpMethod.POST) + .hasImplicitPayloadMembers(true) + .build(); + private static final AwsJsonProtocolMetadata METADATA = + AwsJsonProtocolMetadata.builder() + .protocol(AwsJsonProtocol.AWS_JSON) + .contentType(CONTENT_TYPE) + .build(); + + @Test + void nullPayloadField_notRequired_isSkipped() { + SdkField field = payloadField("OptionalField", obj -> null); + SdkPojo pojo = new SimplePojo(field); + + SdkHttpFullRequest result = createMarshaller().marshall(pojo); + + String body = bodyAsString(result); + assertThat(body).doesNotContain("OptionalField"); + } + + @Test + void nullPayloadField_required_throwsIllegalArgumentException() { + SdkField field = SdkField.builder(MarshallingType.STRING) + .memberName("RequiredField") + .getter(obj -> null) + .setter((obj, val) -> { }) + .traits(LocationTrait.builder() + .location(MarshallLocation.PAYLOAD) + .locationName("RequiredField") + .build(), + RequiredTrait.create()) + .build(); + + SdkPojo pojo = new SimplePojo(field); + + assertThatThrownBy(() -> createMarshaller().marshall(pojo)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("RequiredField") + .hasMessageContaining("must not be null"); + } + + @Test + void nonNullPayloadField_isSerialized() { + SdkField field = payloadField("Name", obj -> "hello"); + SdkPojo pojo = new SimplePojo(field); + + SdkHttpFullRequest result = createMarshaller().marshall(pojo); + + String body = bodyAsString(result); + assertThat(body).contains("\"Name\""); + assertThat(body).contains("\"hello\""); + } + + @Test + void nullNonPayloadField_stillGoesToMarshallField() { + SdkField field = SdkField.builder(MarshallingType.STRING) + .memberName("HeaderField") + .getter(obj -> null) + .setter((obj, val) -> { }) + .traits(LocationTrait.builder() + .location(MarshallLocation.HEADER) + .locationName("x-custom-header") + .build()) + .build(); + + SdkPojo pojo = new SimplePojo(field); + + assertThatNoException().isThrownBy( + () -> createMarshaller().marshall(pojo)); + } + + @Test + void nullPathField_notRequired_stillThrows() { + SdkField field = SdkField.builder(MarshallingType.STRING) + .memberName("PathParam") + .getter(obj -> null) + .setter((obj, val) -> { }) + .traits(LocationTrait.builder() + .location(MarshallLocation.PATH) + .locationName("PathParam") + .build()) + .build(); + + SdkPojo pojo = new SimplePojo(field); + + assertThatThrownBy(() -> createMarshaller().marshall(pojo)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("PathParam") + .hasMessageContaining("must not be null"); + } + + private static SdkField payloadField(String name, + java.util.function.Function getter) { + return SdkField.builder(MarshallingType.STRING) + .memberName(name) + .getter(getter) + .setter((obj, val) -> { }) + .traits(LocationTrait.builder() + .location(MarshallLocation.PAYLOAD) + .locationName(name) + .build()) + .build(); + } + + private static ProtocolMarshaller createMarshaller() { + return JsonProtocolMarshallerBuilder.create() + .endpoint(ENDPOINT) + .jsonGenerator(AwsStructuredPlainJsonFactory + .SDK_JSON_FACTORY.createWriter(CONTENT_TYPE)) + .contentType(CONTENT_TYPE) + .operationInfo(OP_INFO) + .sendExplicitNullForPayload(false) + .protocolMetadata(METADATA) + .build(); + } + + private static String bodyAsString(SdkHttpFullRequest request) { + return request.contentStreamProvider() + .map(p -> { + try { + return software.amazon.awssdk.utils.IoUtils.toUtf8String(p.newStream()); + } catch (Exception e) { + throw new RuntimeException(e); + } + }) + .orElse(""); + } + + private static final class SimplePojo implements SdkPojo { + private final List> fields; + + SimplePojo(SdkField... fields) { + this.fields = Arrays.asList(fields); + } + + @Override + public List> sdkFields() { + return fields; + } + + @Override + public boolean equalsBySdkFields(Object other) { + return other instanceof SimplePojo; + } + + @Override + public Map> sdkFieldNameToField() { + return Collections.emptyMap(); + } + } +} From 5fb2ad74d520fe5448241b8d89f1028d3cd8740b Mon Sep 17 00:00:00 2001 From: RanVaknin <50976344+RanVaknin@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:45:28 -0700 Subject: [PATCH 5/5] Add changelog --- .changes/next-release/bugfix-AWSSDKforJavav2-1a353a6.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changes/next-release/bugfix-AWSSDKforJavav2-1a353a6.json diff --git a/.changes/next-release/bugfix-AWSSDKforJavav2-1a353a6.json b/.changes/next-release/bugfix-AWSSDKforJavav2-1a353a6.json new file mode 100644 index 000000000000..f6615259e601 --- /dev/null +++ b/.changes/next-release/bugfix-AWSSDKforJavav2-1a353a6.json @@ -0,0 +1,6 @@ +{ + "type": "bugfix", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "Optimized JSON serialization by skipping null field marshalling for payload fields" +}