diff --git a/.changes/next-release/bugfix-AWSSDKforJavav2-e2de7f3.json b/.changes/next-release/bugfix-AWSSDKforJavav2-e2de7f3.json new file mode 100644 index 000000000000..3abb4aec1af2 --- /dev/null +++ b/.changes/next-release/bugfix-AWSSDKforJavav2-e2de7f3.json @@ -0,0 +1,6 @@ +{ + "category": "AWS SDK for Java v2", + "contributor": "", + "type": "bugfix", + "description": "Fix a regression for the JSON REST protocol for which an structure explicit payload member was set to the empty object instead of null" +} diff --git a/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/unmarshall/JsonProtocolUnmarshaller.java b/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/unmarshall/JsonProtocolUnmarshaller.java index 526d205ca221..5f2f129e8571 100644 --- a/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/unmarshall/JsonProtocolUnmarshaller.java +++ b/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/unmarshall/JsonProtocolUnmarshaller.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.Supplier; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.annotations.ThreadSafe; import software.amazon.awssdk.core.SdkBytes; @@ -266,6 +267,11 @@ private T unmarshallFromJson(SdkPojo sdkPojo, InputStream in return (T) unmarshallingParser.parse(sdkPojo, inputStream); } + @SuppressWarnings("unchecked") + private T unmarshallMemberFromJson(Supplier constructor, InputStream inputStream) { + return (T) unmarshallingParser.parseMember(constructor, inputStream); + } + private TypeT unmarshallResponse(SdkPojo sdkPojo, SdkHttpFullResponse response) throws IOException { JsonUnmarshallerContext context = JsonUnmarshallerContext.builder() @@ -290,7 +296,7 @@ private TypeT unmarshallResponse(SdkPojo sdkPojo, } else if (isExplicitPayloadMember(field) && field.marshallingType() == MarshallingType.SDK_POJO) { Optional responseContent = context.response().content(); if (responseContent.isPresent()) { - field.set(sdkPojo, unmarshallFromJson(field.constructor().get(), responseContent.get())); + field.set(sdkPojo, unmarshallMemberFromJson(field.constructor(), responseContent.get())); } else { field.set(sdkPojo, null); } diff --git a/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/unmarshall/JsonUnmarshallingParser.java b/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/unmarshall/JsonUnmarshallingParser.java index 30737ae78d73..77e6231dd2a5 100644 --- a/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/unmarshall/JsonUnmarshallingParser.java +++ b/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/unmarshall/JsonUnmarshallingParser.java @@ -24,6 +24,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.function.Supplier; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.annotations.ThreadSafe; import software.amazon.awssdk.core.SdkBytes; @@ -72,6 +73,33 @@ public static Builder builder() { return new Builder(); } + /** + * Parse the provided {@link InputStream} and return the deserialized {@link SdkPojo}. Unlike + * {@link #parse(SdkPojo, InputStream)} this method returns null if the input stream is empty. This is used to unmarshall + * payload members that can be null unlike top-level response pojos. + */ + public SdkPojo parseMember(Supplier constructor, InputStream content) { + return invokeSafely(() -> { + try (JsonParser parser = jsonFactory.createParser(content) + .configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false)) { + + JsonUnmarshallerContext c = JsonUnmarshallerContext.builder().build(); + JsonToken token = parser.nextToken(); + if (token == null) { + return null; + } + if (token == JsonToken.VALUE_NULL) { + return null; + } + if (token != JsonToken.START_OBJECT) { + throw new JsonParseException("expecting start object, got instead: " + token); + } + SdkPojo pojo = constructor.get(); + return parseSdkPojo(c, pojo, parser); + } + }); + } + /** * Parse the provided {@link InputStream} and return the deserialized {@link SdkPojo}. */ diff --git a/test/protocol-tests-core/src/main/resources/software/amazon/awssdk/protocol/suites/cases/rest-json-output.json b/test/protocol-tests-core/src/main/resources/software/amazon/awssdk/protocol/suites/cases/rest-json-output.json index d660230cf715..357a6605be5c 100644 --- a/test/protocol-tests-core/src/main/resources/software/amazon/awssdk/protocol/suites/cases/rest-json-output.json +++ b/test/protocol-tests-core/src/main/resources/software/amazon/awssdk/protocol/suites/cases/rest-json-output.json @@ -19,6 +19,24 @@ } } }, + { + "description": "Operation with explicit payload structure, with emtpy output is unmarshalled as null value", + "given": { + "response": { + "status_code": 200, + "body": "" + } + }, + "when": { + "action": "unmarshall", + "operation": "OperationWithExplicitPayloadStructure" + }, + "then": { + "deserializedAs": { + "PayloadMember": null + } + } + }, { "description": "Operation with streaming payload in output is unmarshalled correctly", "given": {