diff --git a/.changes/next-release/bugfix-AWSSDKforJavav2-47e105f.json b/.changes/next-release/bugfix-AWSSDKforJavav2-47e105f.json new file mode 100644 index 000000000000..1c52918c731c --- /dev/null +++ b/.changes/next-release/bugfix-AWSSDKforJavav2-47e105f.json @@ -0,0 +1,6 @@ +{ + "category": "AWS SDK for Java v2", + "contributor": "", + "type": "bugfix", + "description": "Update the REST-JSON marshalling logic to conform to the standard expected behavior WRT to the `Content-Type` of the request." +} diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/ShapeModel.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/ShapeModel.java index 26b797637805..43deea8d5ba7 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/ShapeModel.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/ShapeModel.java @@ -187,11 +187,17 @@ public List getUnboundMembers() { List unboundMembers = new ArrayList<>(); if (members != null) { for (MemberModel member : members) { - if (member.getHttp().getLocation() == null) { + if (member.getHttp().getLocation() == null && !member.getHttp().getIsPayload()) { if (hasPayloadMember) { + // There is an explicit payload, but this unbound + // member isn't it. + // Note: Somewhat unintuitive, explicit payloads don't + // have an explicit location; they're identified by + // the payload HTTP trait being true. throw new IllegalStateException(String.format( - "C2J Shape %s has both an explicit payload member and unbound (no explicit location) members. " - + "This is undefined behavior, verify the correctness of the C2J model", c2jName)); + "C2J Shape %s has both an explicit payload member and unbound (no explicit location) member, %s." + + " This is undefined behavior, verify the correctness of the C2J model.", + c2jName, member.getName())); } unboundMembers.add(member); } @@ -221,7 +227,12 @@ public List getUnboundEventMembers() { public boolean hasPayloadMembers() { return hasPayloadMember || getExplicitEventPayloadMember() != null || - !getUnboundMembers().isEmpty() || + hasImplicitPayloadMembers(); + + } + + public boolean hasImplicitPayloadMembers() { + return !getUnboundMembers().isEmpty() || (isEvent() && !getUnboundEventMembers().isEmpty()); } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/transform/protocols/JsonMarshallerSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/transform/protocols/JsonMarshallerSpec.java index be28936b5a8d..eaed3ee87135 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/transform/protocols/JsonMarshallerSpec.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/transform/protocols/JsonMarshallerSpec.java @@ -95,6 +95,7 @@ protected FieldSpec operationInfoField() { .add(".httpMethod($T.$L)", SdkHttpMethod.class, shapeModel.getMarshaller().getVerb()) .add(".hasExplicitPayloadMember($L)", shapeModel.isHasPayloadMember() || shapeModel.getExplicitEventPayloadMember() != null) + .add(".hasImplicitPayloadMembers($L)", shapeModel.hasImplicitPayloadMembers()) .add(".hasPayloadMembers($L)", shapeModel.hasPayloadMembers()); if (StringUtils.isNotBlank(shapeModel.getMarshaller().getTarget())) { diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/transform/alltypesrequestmarshaller.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/transform/alltypesrequestmarshaller.java index 1666fa85e098..4fd6fff40c66 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/transform/alltypesrequestmarshaller.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/transform/alltypesrequestmarshaller.java @@ -19,7 +19,8 @@ @SdkInternalApi public class AllTypesRequestMarshaller implements Marshaller { private static final OperationInfo SDK_OPERATION_BINDING = OperationInfo.builder().requestUri("/") - .httpMethod(SdkHttpMethod.POST).hasExplicitPayloadMember(false).hasPayloadMembers(true).build(); + .httpMethod(SdkHttpMethod.POST).hasExplicitPayloadMember(false).hasImplicitPayloadMembers(true) + .hasPayloadMembers(true).build(); private final BaseAwsJsonProtocolFactory protocolFactory; @@ -32,11 +33,10 @@ public SdkHttpFullRequest marshall(AllTypesRequest allTypesRequest) { Validate.paramNotNull(allTypesRequest, "allTypesRequest"); try { ProtocolMarshaller protocolMarshaller = protocolFactory - .createProtocolMarshaller(SDK_OPERATION_BINDING); + .createProtocolMarshaller(SDK_OPERATION_BINDING); return protocolMarshaller.marshall(allTypesRequest); } catch (Exception e) { throw SdkClientException.builder().message("Unable to marshall request to JSON: " + e.getMessage()).cause(e).build(); } } } - diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/transform/eventstreamoperationrequestmarshaller.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/transform/eventstreamoperationrequestmarshaller.java index e62c6efe66d7..4d546227a0ed 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/transform/eventstreamoperationrequestmarshaller.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/transform/eventstreamoperationrequestmarshaller.java @@ -19,8 +19,8 @@ @SdkInternalApi public class EventStreamOperationRequestMarshaller implements Marshaller { private static final OperationInfo SDK_OPERATION_BINDING = OperationInfo.builder() - .requestUri("/2016-03-11/eventStreamOperation").httpMethod(SdkHttpMethod.POST).hasExplicitPayloadMember(true) - .hasPayloadMembers(true).hasEventStreamingInput(true).build(); + .requestUri("/2016-03-11/eventStreamOperation").httpMethod(SdkHttpMethod.POST).hasExplicitPayloadMember(true) + .hasImplicitPayloadMembers(false).hasPayloadMembers(true).hasEventStreamingInput(true).build(); private final BaseAwsJsonProtocolFactory protocolFactory; @@ -33,7 +33,7 @@ public SdkHttpFullRequest marshall(EventStreamOperationRequest eventStreamOperat Validate.paramNotNull(eventStreamOperationRequest, "eventStreamOperationRequest"); try { ProtocolMarshaller protocolMarshaller = protocolFactory - .createProtocolMarshaller(SDK_OPERATION_BINDING); + .createProtocolMarshaller(SDK_OPERATION_BINDING); return protocolMarshaller.marshall(eventStreamOperationRequest); } catch (Exception e) { throw SdkClientException.builder().message("Unable to marshall request to JSON: " + e.getMessage()).cause(e).build(); diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/transform/eventstreamoperationwithonlyinputrequestmarshaller.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/transform/eventstreamoperationwithonlyinputrequestmarshaller.java index e96817e5bfca..eb88d39c638b 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/transform/eventstreamoperationwithonlyinputrequestmarshaller.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/transform/eventstreamoperationwithonlyinputrequestmarshaller.java @@ -17,11 +17,11 @@ */ @Generated("software.amazon.awssdk:codegen") @SdkInternalApi -public class EventStreamOperationWithOnlyInputRequestMarshaller implements - Marshaller { +public class EventStreamOperationWithOnlyInputRequestMarshaller implements Marshaller { private static final OperationInfo SDK_OPERATION_BINDING = OperationInfo.builder() - .requestUri("/2016-03-11/EventStreamOperationWithOnlyInput").httpMethod(SdkHttpMethod.POST) - .hasExplicitPayloadMember(false).hasPayloadMembers(true).hasEventStreamingInput(true).build(); + .requestUri("/2016-03-11/EventStreamOperationWithOnlyInput").httpMethod(SdkHttpMethod.POST) + .hasExplicitPayloadMember(false).hasImplicitPayloadMembers(true).hasPayloadMembers(true).hasEventStreamingInput(true) + .build(); private final BaseAwsJsonProtocolFactory protocolFactory; @@ -34,11 +34,10 @@ public SdkHttpFullRequest marshall(EventStreamOperationWithOnlyInputRequest even Validate.paramNotNull(eventStreamOperationWithOnlyInputRequest, "eventStreamOperationWithOnlyInputRequest"); try { ProtocolMarshaller protocolMarshaller = protocolFactory - .createProtocolMarshaller(SDK_OPERATION_BINDING); + .createProtocolMarshaller(SDK_OPERATION_BINDING); return protocolMarshaller.marshall(eventStreamOperationWithOnlyInputRequest); } catch (Exception e) { throw SdkClientException.builder().message("Unable to marshall request to JSON: " + e.getMessage()).cause(e).build(); } } } - diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/transform/nestedcontainersrequestmarshaller.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/transform/nestedcontainersrequestmarshaller.java index 6f532a7565da..c8a1fe310b22 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/transform/nestedcontainersrequestmarshaller.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/transform/nestedcontainersrequestmarshaller.java @@ -19,7 +19,8 @@ @SdkInternalApi public class NestedContainersRequestMarshaller implements Marshaller { private static final OperationInfo SDK_OPERATION_BINDING = OperationInfo.builder().requestUri("/") - .httpMethod(SdkHttpMethod.POST).hasExplicitPayloadMember(false).hasPayloadMembers(true).build(); + .httpMethod(SdkHttpMethod.POST).hasExplicitPayloadMember(false).hasImplicitPayloadMembers(true) + .hasPayloadMembers(true).build(); private final BaseAwsJsonProtocolFactory protocolFactory; @@ -32,11 +33,10 @@ public SdkHttpFullRequest marshall(NestedContainersRequest nestedContainersReque Validate.paramNotNull(nestedContainersRequest, "nestedContainersRequest"); try { ProtocolMarshaller protocolMarshaller = protocolFactory - .createProtocolMarshaller(SDK_OPERATION_BINDING); + .createProtocolMarshaller(SDK_OPERATION_BINDING); return protocolMarshaller.marshall(nestedContainersRequest); } catch (Exception e) { throw SdkClientException.builder().message("Unable to marshall request to JSON: " + e.getMessage()).cause(e).build(); } } } - diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/transform/operationwithnoinputoroutputrequestmarshaller.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/transform/operationwithnoinputoroutputrequestmarshaller.java index f1a30e56ffcd..8247936f0b4c 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/transform/operationwithnoinputoroutputrequestmarshaller.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/transform/operationwithnoinputoroutputrequestmarshaller.java @@ -17,10 +17,10 @@ */ @Generated("software.amazon.awssdk:codegen") @SdkInternalApi -public class OperationWithNoInputOrOutputRequestMarshaller implements - Marshaller { +public class OperationWithNoInputOrOutputRequestMarshaller implements Marshaller { private static final OperationInfo SDK_OPERATION_BINDING = OperationInfo.builder().requestUri("/") - .httpMethod(SdkHttpMethod.POST).hasExplicitPayloadMember(false).hasPayloadMembers(false).build(); + .httpMethod(SdkHttpMethod.POST).hasExplicitPayloadMember(false).hasImplicitPayloadMembers(false) + .hasPayloadMembers(false).build(); private final BaseAwsJsonProtocolFactory protocolFactory; @@ -33,11 +33,10 @@ public SdkHttpFullRequest marshall(OperationWithNoInputOrOutputRequest operation Validate.paramNotNull(operationWithNoInputOrOutputRequest, "operationWithNoInputOrOutputRequest"); try { ProtocolMarshaller protocolMarshaller = protocolFactory - .createProtocolMarshaller(SDK_OPERATION_BINDING); + .createProtocolMarshaller(SDK_OPERATION_BINDING); return protocolMarshaller.marshall(operationWithNoInputOrOutputRequest); } catch (Exception e) { throw SdkClientException.builder().message("Unable to marshall request to JSON: " + e.getMessage()).cause(e).build(); } } } - diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/transform/streaminginputoperationrequestmarshaller.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/transform/streaminginputoperationrequestmarshaller.java index 8513a752adc1..79101f27edc4 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/transform/streaminginputoperationrequestmarshaller.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/transform/streaminginputoperationrequestmarshaller.java @@ -19,8 +19,8 @@ @SdkInternalApi public class StreamingInputOperationRequestMarshaller implements Marshaller { private static final OperationInfo SDK_OPERATION_BINDING = OperationInfo.builder() - .requestUri("/2016-03-11/streamingInputOperation").httpMethod(SdkHttpMethod.POST).hasExplicitPayloadMember(true) - .hasPayloadMembers(true).hasStreamingInput(true).build(); + .requestUri("/2016-03-11/streamingInputOperation").httpMethod(SdkHttpMethod.POST).hasExplicitPayloadMember(true) + .hasImplicitPayloadMembers(false).hasPayloadMembers(true).hasStreamingInput(true).build(); private final BaseAwsJsonProtocolFactory protocolFactory; @@ -33,11 +33,10 @@ public SdkHttpFullRequest marshall(StreamingInputOperationRequest streamingInput Validate.paramNotNull(streamingInputOperationRequest, "streamingInputOperationRequest"); try { ProtocolMarshaller protocolMarshaller = protocolFactory - .createProtocolMarshaller(SDK_OPERATION_BINDING); + .createProtocolMarshaller(SDK_OPERATION_BINDING); return protocolMarshaller.marshall(streamingInputOperationRequest); } catch (Exception e) { throw SdkClientException.builder().message("Unable to marshall request to JSON: " + e.getMessage()).cause(e).build(); } } } - diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/transform/streamingoutputoperationrequestmarshaller.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/transform/streamingoutputoperationrequestmarshaller.java index 06c982a0fe49..1ffe8a4471a7 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/transform/streamingoutputoperationrequestmarshaller.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/transform/streamingoutputoperationrequestmarshaller.java @@ -19,8 +19,8 @@ @SdkInternalApi public class StreamingOutputOperationRequestMarshaller implements Marshaller { private static final OperationInfo SDK_OPERATION_BINDING = OperationInfo.builder() - .requestUri("/2016-03-11/streamingOutputOperation").httpMethod(SdkHttpMethod.POST).hasExplicitPayloadMember(false) - .hasPayloadMembers(false).build(); + .requestUri("/2016-03-11/streamingOutputOperation").httpMethod(SdkHttpMethod.POST).hasExplicitPayloadMember(false) + .hasImplicitPayloadMembers(false).hasPayloadMembers(false).build(); private final BaseAwsJsonProtocolFactory protocolFactory; @@ -33,11 +33,10 @@ public SdkHttpFullRequest marshall(StreamingOutputOperationRequest streamingOutp Validate.paramNotNull(streamingOutputOperationRequest, "streamingOutputOperationRequest"); try { ProtocolMarshaller protocolMarshaller = protocolFactory - .createProtocolMarshaller(SDK_OPERATION_BINDING); + .createProtocolMarshaller(SDK_OPERATION_BINDING); return protocolMarshaller.marshall(streamingOutputOperationRequest); } catch (Exception e) { throw SdkClientException.builder().message("Unable to marshall request to JSON: " + e.getMessage()).cause(e).build(); } } } - diff --git a/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/DefaultJsonContentTypeResolver.java b/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/DefaultJsonContentTypeResolver.java index 12b6eda2b838..586803f13c0b 100644 --- a/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/DefaultJsonContentTypeResolver.java +++ b/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/DefaultJsonContentTypeResolver.java @@ -23,6 +23,7 @@ */ @SdkProtectedApi public class DefaultJsonContentTypeResolver implements JsonContentTypeResolver { + private static final String REST_JSON_CONTENT_TYPE = "application/json"; private final String prefix; @@ -32,7 +33,9 @@ public DefaultJsonContentTypeResolver(String prefix) { @Override public String resolveContentType(AwsJsonProtocolMetadata protocolMetadata) { - //Changing this to 'application/json' may break clients expecting 'application/x-amz-json-1.1' + if (AwsJsonProtocol.REST_JSON.equals(protocolMetadata.protocol())) { + return REST_JSON_CONTENT_TYPE; + } return prefix + protocolMetadata.protocolVersion(); } } 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 aaee61c14e9d..509b0c8af92f 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 @@ -16,7 +16,6 @@ package software.amazon.awssdk.protocols.json.internal.marshall; import static software.amazon.awssdk.core.internal.util.Mimetype.MIMETYPE_EVENT_STREAM; -import static software.amazon.awssdk.core.protocol.MarshallingType.DOCUMENT; import static software.amazon.awssdk.http.Header.CHUNKED; import static software.amazon.awssdk.http.Header.CONTENT_LENGTH; import static software.amazon.awssdk.http.Header.CONTENT_TYPE; @@ -63,6 +62,7 @@ public class JsonProtocolMarshaller implements ProtocolMarshaller field : pojo.sdkFields()) { Object val = field.getValueOrDefault(pojo); - if (isBinary(field, val)) { - request.contentStreamProvider(((SdkBytes) val)::asInputStream); - } else if (isDocumentType(field) && val != null) { - marshalDocumentType(field, val); - } else { - if (val != null && field.containsTrait(PayloadTrait.class)) { - jsonGenerator.writeStartObject(); - doMarshall((SdkPojo) val); - jsonGenerator.writeEndObject(); - } else { - MARSHALLER_REGISTRY.getMarshaller(field.location(), field.marshallingType(), val) - .marshall(val, marshallerContext, field.locationName(), (SdkField) field); + if (isExplicitBinaryPayload(field)) { + if (val != null) { + request.contentStreamProvider(((SdkBytes) val)::asInputStream); } + } else if (isExplicitPayloadMember(field)) { + marshallExplicitJsonPayload(field, val); + } else { + marshallField(field, val); } } } - private boolean isBinary(SdkField field, Object val) { - return isExplicitPayloadMember(field) && val instanceof SdkBytes; + private boolean isExplicitBinaryPayload(SdkField field) { + return isExplicitPayloadMember(field) && MarshallingType.SDK_BYTES.equals(field.marshallingType()); } private boolean isExplicitPayloadMember(SdkField field) { return field.containsTrait(PayloadTrait.class); } + private void marshallExplicitJsonPayload(SdkField field, Object val) { + // Explicit JSON payloads are always marshalled as an object, + // even if they're null, in which case it's an empty object. + jsonGenerator.writeStartObject(); + if (val != null) { + if (MarshallingType.DOCUMENT.equals(field.marshallingType())) { + marshallField(field, val); + } else { + doMarshall((SdkPojo) val); + } + } + jsonGenerator.writeEndObject(); + } + @Override public SdkHttpFullRequest marshall(SdkPojo pojo) { startMarshalling(); @@ -212,7 +223,7 @@ private SdkHttpFullRequest finishMarshalling() { // Content may already be set if the payload is binary data. if (request.contentStreamProvider() == null) { // End the implicit request object if needed. - if (!hasExplicitPayloadMember) { + if (needTopLevelJsonObject()) { jsonGenerator.writeEndObject(); } @@ -243,7 +254,7 @@ private SdkHttpFullRequest finishMarshalling() { } request.removeHeader(CONTENT_LENGTH); request.putHeader(TRANSFER_ENCODING, CHUNKED); - } else if (contentType != null && !hasStreamingInput && request.contentStreamProvider() != null) { + } else if (contentType != null && !hasStreamingInput && request.headers().containsKey(CONTENT_LENGTH)) { request.putHeader(CONTENT_TYPE, contentType); } } @@ -251,19 +262,14 @@ private SdkHttpFullRequest finishMarshalling() { return request.build(); } - private boolean isDocumentType(SdkField field) { - return DOCUMENT.equals(field.marshallingType()); - } - - private void marshalDocumentType(SdkField field, Object val) { - boolean isExplicitPayloadField = hasExplicitPayloadMember && field.containsTrait(PayloadTrait.class); - if (isExplicitPayloadField) { - jsonGenerator.writeStartObject(); - } + private void marshallField(SdkField field, Object val) { MARSHALLER_REGISTRY.getMarshaller(field.location(), field.marshallingType(), val) .marshall(val, marshallerContext, field.locationName(), (SdkField) field); - if (isExplicitPayloadField) { - jsonGenerator.writeEndObject(); - } + } + + private boolean needTopLevelJsonObject() { + return AwsJsonProtocol.AWS_JSON.equals(protocolMetadata.protocol()) + || (!hasExplicitPayloadMember && hasImplicitPayloadMembers); + } } diff --git a/core/protocols/protocol-core/src/main/java/software/amazon/awssdk/protocols/core/OperationInfo.java b/core/protocols/protocol-core/src/main/java/software/amazon/awssdk/protocols/core/OperationInfo.java index 8b4dbf287e49..1a55ee8c9dca 100644 --- a/core/protocols/protocol-core/src/main/java/software/amazon/awssdk/protocols/core/OperationInfo.java +++ b/core/protocols/protocol-core/src/main/java/software/amazon/awssdk/protocols/core/OperationInfo.java @@ -31,6 +31,7 @@ public final class OperationInfo { private final String apiVersion; private final boolean hasExplicitPayloadMember; private final boolean hasPayloadMembers; + private final boolean hasImplicitPayloadMembers; private final boolean hasStreamingInput; private final boolean hasEventStreamingInput; private final boolean hasEvent; @@ -42,6 +43,7 @@ private OperationInfo(Builder builder) { this.operationIdentifier = builder.operationIdentifier; this.apiVersion = builder.apiVersion; this.hasExplicitPayloadMember = builder.hasExplicitPayloadMember; + this.hasImplicitPayloadMembers = builder.hasImplicitPayloadMembers; this.hasPayloadMembers = builder.hasPayloadMembers; this.hasStreamingInput = builder.hasStreamingInput; this.additionalMetadata = builder.additionalMetadata.build(); @@ -95,6 +97,14 @@ public boolean hasPayloadMembers() { return hasPayloadMembers; } + /** + * @return True if the operation has members that are not explicitly bound to a marshalling location, and thus are + * implicitly bound to the body. + */ + public boolean hasImplicitPayloadMembers() { + return hasImplicitPayloadMembers; + } + /** * @return True if the operation has streaming input. */ @@ -144,6 +154,7 @@ public static final class Builder { private String operationIdentifier; private String apiVersion; private boolean hasExplicitPayloadMember; + private boolean hasImplicitPayloadMembers; private boolean hasPayloadMembers; private boolean hasStreamingInput; private boolean hasEventStreamingInput; @@ -183,6 +194,11 @@ public Builder hasPayloadMembers(boolean hasPayloadMembers) { return this; } + public Builder hasImplicitPayloadMembers(boolean hasImplicitPayloadMembers) { + this.hasImplicitPayloadMembers = hasImplicitPayloadMembers; + return this; + } + public Builder hasStreamingInput(boolean hasStreamingInput) { this.hasStreamingInput = hasStreamingInput; return this; diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/documenttype/DocumentTypeTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/documenttype/DocumentTypeTest.java index 0afe6b8f7e7e..b2e4a65125fe 100644 --- a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/documenttype/DocumentTypeTest.java +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/documenttype/DocumentTypeTest.java @@ -223,7 +223,7 @@ public void explicitDocumentOnlyNullPayload() { WithExplicitDocumentPayloadResponse response = jsonClient.withExplicitDocumentPayload(c -> c.myDocument(null).accept(AcceptHeader.IMAGE_JPEG)); String syncRequest = getSyncRequestBody(); - assertThat(syncRequest).isEmpty(); + assertThat(syncRequest).isEqualTo("{}"); SdkHttpRequest sdkHttpRequest = getSyncRequest(); assertThat(sdkHttpRequest.firstMatchingHeader("accept").get()).contains(AcceptHeader.IMAGE_JPEG.toString()); assertThat(response.myDocument()).isNull(); @@ -236,7 +236,7 @@ public void explicitDocumentOnlyEmptyPayload() { WithExplicitDocumentPayloadResponse response = jsonClient.withExplicitDocumentPayload(c -> c.myDocument(null).accept(AcceptHeader.IMAGE_JPEG)); String syncRequest = getSyncRequestBody(); - assertThat(syncRequest).isEmpty(); + assertThat(syncRequest).isEqualTo("{}"); SdkHttpRequest sdkHttpRequest = getSyncRequest(); assertThat(sdkHttpRequest.firstMatchingHeader("accept").get()).contains(AcceptHeader.IMAGE_JPEG.toString()); assertThat(response.myDocument()).isNull(); diff --git a/test/protocol-tests-core/src/main/resources/software/amazon/awssdk/protocol/suites/cases/rest-json-contenttype.json b/test/protocol-tests-core/src/main/resources/software/amazon/awssdk/protocol/suites/cases/rest-json-contenttype.json new file mode 100644 index 000000000000..b82b0f70e93b --- /dev/null +++ b/test/protocol-tests-core/src/main/resources/software/amazon/awssdk/protocol/suites/cases/rest-json-contenttype.json @@ -0,0 +1,301 @@ +[ + { + "description": "TestBody", + "given": { + "input": { + "testConfig": { + "timeout": 10 + }, + "testId": "t-12345" + } + }, + "when": { + "action": "marshall", + "operation": "TestBody" + }, + "then": { + "serializedAs": { + "uri": "/body", + "method": "POST", + "headers": { + "contains": { + "x-amz-test-id": "t-12345", + "Content-Type": "application/json" + } + }, + "body": { + "jsonEquals": "{\"testConfig\": {\"timeout\": 10}}" + } + } + } +}, +{ + "description": "TestPayloadNoParams", + "given": { + "input":{ + } + }, + "when": { + "action": "marshall", + "operation": "TestPayload" + }, + "then": { + "serializedAs": { + "uri": "/payload", + "method": "POST", + "headers": { + "contains": { + "Content-Type": "application/json" + } + }, + "body": { + "jsonEquals": "{}" + } + } + } +}, +{ + "description": "TestPayload", + "given": { + "input": { + "payloadConfig": { + "data": 25 + }, + "testId": "t-12345" + } + }, + "when": { + "action": "marshall", + "operation": "TestPayload" + }, + "then": { + "serializedAs": { + "uri": "/payload", + "method": "POST", + "headers": { + "contains": { + "x-amz-test-id": "t-12345", + "Content-Type": "application/json" + } + }, + "body": { + "jsonEquals": "{\"data\": 25}" + } + } + } +}, +{ + "description": "TestPayloadNoBody", + "given": { + "input": { + "testId": "t-12345" + } + }, + "when": { + "action":"marshall", + "operation":"TestPayload" + }, + "then": { + "serializedAs": { + "uri": "/payload", + "method": "POST", + "headers": { + "contains": { + "x-amz-test-id": "t-12345", + "Content-Type": "application/json" + } + }, + "body": { + "jsonEquals": "{}" + } + } + } +}, +{ + "description": "TestBlobPayload", + "given": { + "input": { + "data": "1234", + "contentType": "image/jpg" + } + }, + "when": { + "action": "marshall", + "operation": "TestBlobPayload" + }, + "then": { + "serializedAs": { + "uri": "/blob-payload", + "method": "POST", + "headers": { + "contains": { + "Content-Type": "image/jpg" + } + }, + "body": { + "equals": "1234" + } + } + } +}, +{ + "description": "TestBlobPayloadNoParams", + "given": { + "input": { + } + }, + "when": { + "action": "marshall", + "operation": "TestBlobPayload" + }, + "then": { + "serializedAs": { + "uri": "/blob-payload", + "method": "POST", + "headers": { + "doesNotContain": [ + "Content-Type" + ] + }, + "body": { + "equals": "" + } + } + } +}, +{ + "description": "NoPayload", + "given": { + "input": { + } + }, + "when": { + "action": "marshall", + "operation": "NoPayload" + }, + "then": { + "serializedAs": { + "uri": "/no-payload", + "method": "GET", + "headers": { + "doesNotContain": [ + "Content-Type", + "Content-Length" + ] + }, + "body": { + "equals": "" + } + } + } +}, +{ + "description": "NoPayloadWithHeader", + "given": { + "input": { + "testId": "t-12345" + } + }, + "when": { + "action": "marshall", + "operation": "NoPayload" + }, + "then": { + "serializedAs": { + "uri": "/no-payload", + "method": "GET", + "headers": { + "contains": { + "x-amz-test-id": "t-12345" + }, + "doesNotContain": [ + "Content-Type", + "Content-Length" + ] + }, + "body": { + "equals": "" + } + } + } +}, +{ + "description": "NoPayloadGet", + "given": { + "input": { + } + }, + "when": { + "action": "marshall", + "operation": "NoPayloadPost" + }, + "then": { + "serializedAs": { + "uri": "/no-payload", + "method": "POST", + "headers": { + "doesNotContain": [ + "Content-Type" + ] + }, + "body": { + "equals": "" + } + } + } +}, +{ + "description": "NoPayloadGetWithHeader", + "given": { + "input": { + "testId": "t-12345" + } + }, + "when": { + "action": "marshall", + "operation": "NoPayloadPost" + }, + "then": { + "serializedAs": { + "uri": "/no-payload", + "method": "POST", + "headers": { + "contains": { + "x-amz-test-id": "t-12345" + }, + "doesNotContain": [ + "Content-Type" + ] + }, + "body": { + "equals": "" + } + } + } +}, +{ + "description": "TestBodyNoParams", + "given": { + "input": { + } + }, + "when": { + "action": "marshall", + "operation": "TestBody" + }, + "then": { + "serializedAs": { + "uri": "/body", + "method": "POST", + "headers": { + "contains": { + "Content-Type": "application/json" + } + }, + "body": { + "jsonEquals": "{}" + } + } + } +} +] diff --git a/test/protocol-tests-core/src/main/resources/software/amazon/awssdk/protocol/suites/cases/rest-json-input.json b/test/protocol-tests-core/src/main/resources/software/amazon/awssdk/protocol/suites/cases/rest-json-input.json index 9bd43b7b9e56..c5046904386e 100644 --- a/test/protocol-tests-core/src/main/resources/software/amazon/awssdk/protocol/suites/cases/rest-json-input.json +++ b/test/protocol-tests-core/src/main/resources/software/amazon/awssdk/protocol/suites/cases/rest-json-input.json @@ -59,7 +59,7 @@ } }, { - "description": "Explicit payload member and no parameters marshalls into an empty request body", + "description": "Explicit payload member and no parameters marshalls into an empty JSON object", "given": { "input": { } @@ -71,7 +71,7 @@ "then": { "serializedAs": { "body": { - "equals": "" + "jsonEquals": "{}" } } } diff --git a/test/protocol-tests-core/src/main/resources/software/amazon/awssdk/protocol/suites/restjson-contenttype-suite.json b/test/protocol-tests-core/src/main/resources/software/amazon/awssdk/protocol/suites/restjson-contenttype-suite.json new file mode 100644 index 000000000000..3455917a66e3 --- /dev/null +++ b/test/protocol-tests-core/src/main/resources/software/amazon/awssdk/protocol/suites/restjson-contenttype-suite.json @@ -0,0 +1,5 @@ +{ + "testCases": [ + "cases/rest-json-contenttype.json" + ] +} \ No newline at end of file diff --git a/test/protocol-tests/src/main/resources/codegen-resources/restjson/contenttype/service-2.json b/test/protocol-tests/src/main/resources/codegen-resources/restjson/contenttype/service-2.json new file mode 100644 index 000000000000..e7770206c97f --- /dev/null +++ b/test/protocol-tests/src/main/resources/codegen-resources/restjson/contenttype/service-2.json @@ -0,0 +1,168 @@ +{ + "version":"2.0", + "metadata":{ + "apiVersion":"2021-05-13", + "endpointPrefix":"rest-test", + "protocol":"rest-json", + "serviceAbbreviation":"AmazonProtocolRestJsonContentType", + "serviceFullName":"Content Type Amazon Protocol Rest Json", + "serviceId":"AmazonProtocolRestJsonContentType", + "signatureVersion":"v4", + "timestampFormat":"unixTimestamp", + "uid":"restjson-contenttype-2021-05-13" + }, + "operations":{ + "TestBody":{ + "name":"TestBody", + "http":{ + "method":"POST", + "requestUri":"/body" + }, + "input":{"shape":"TestBodyRequest"} + }, + "TestPayload": { + "name": "TestPayload", + "http": { + "method": "POST", + "requestUri": "/payload" + }, + "input":{"shape": "TestPayloadRequest"} + }, + "TestBlobPayload": { + "name": "TestBlobPayload", + "http": { + "method": "POST", + "requestUri": "/blob-payload" + }, + "input": {"shape": "TestBlobPayloadRequest"} + }, + "NoPayload": { + "name": "NoPayload", + "http": { + "method": "GET", + "requestUri": "no-payload" + }, + "input": {"shape": "NoPayloadRequest"} + }, + "NoPayloadPost": { + "name": "NoPayloadPost", + "http": { + "method": "POST", + "requestUri": "no-payload" + }, + "input": {"shape": "NoPayloadPostRequest"} + } + }, + "shapes":{ + "Integer":{ + "type":"integer" + }, + "String":{"type":"string"}, + "Blob":{"type":"blob"}, + "NoPayloadRequest":{ + "type":"structure", + "required":[], + "members":{ + "testId":{ + "shape":"TestId", + "documentation":"

The unique ID for a test.

", + "location":"header", + "locationName":"x-amz-test-id" + } + }, + "documentation":"

The request structure for a no payload request.

" + }, + "NoPayloadPostRequest":{ + "type":"structure", + "required":[], + "members":{ + "testId":{ + "shape":"TestId", + "documentation":"

The unique ID for a test.

", + "location":"header", + "locationName":"x-amz-test-id" + } + }, + "documentation":"

The request structure for a no payload request.

" + }, + "TestId":{ + "type":"string", + "max":8, + "min":3, + "pattern":"t-[a-z0-9-]+" + }, + "TestConfig":{ + "type":"structure", + "required":[], + "members":{ + "timeout":{ + "shape":"Integer", + "documentation":"

Timeout in seconds

" + } + } + }, + "PayloadConfig":{ + "type":"structure", + "required":[], + "members":{ + "data":{ + "shape":"Integer", + "documentation":"

Numerical data

" + } + } + }, + "TestBodyRequest":{ + "type":"structure", + "required":[], + "members":{ + "testConfig":{ + "shape":"TestConfig", + "documentation":"

Content to post

" + }, + "testId":{ + "shape":"TestId", + "documentation":"

Optional test identifier

", + "location":"header", + "locationName":"x-amz-test-id" + } + }, + "documentation":"

The request structure for a test body request.

" + }, + "TestPayloadRequest":{ + "type":"structure", + "required":[], + "members":{ + "payloadConfig":{ + "shape":"PayloadConfig", + "documentation":"

Payload to post

" + }, + "testId":{ + "shape":"TestId", + "documentation":"

Optional test identifier

", + "location":"header", + "locationName":"x-amz-test-id" + } + }, + "documentation":"

The request structure for a payload request.

", + "payload":"payloadConfig" + }, + "TestBlobPayloadRequest":{ + "type":"structure", + "required":[], + "members":{ + "data":{ + "shape":"Blob", + "documentation":"

Blob payload to post

" + }, + "contentType":{ + "shape":"String", + "documentation":"

Optional content-type header

", + "location":"header", + "locationName":"Content-Type" + } + }, + "documentation":"

The request structure for a blob payload request.

", + "payload":"data" + } + } +} diff --git a/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/RestJsonContentTypeProtocolTest.java b/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/RestJsonContentTypeProtocolTest.java new file mode 100644 index 000000000000..fe7191e05ba2 --- /dev/null +++ b/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/RestJsonContentTypeProtocolTest.java @@ -0,0 +1,50 @@ +/* + * 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.protocol.tests; + +import java.io.IOException; +import java.util.List; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import software.amazon.awssdk.protocol.ProtocolTestSuiteLoader; +import software.amazon.awssdk.protocol.model.TestCase; +import software.amazon.awssdk.protocol.runners.ProtocolTestRunner; + +@RunWith(Parameterized.class) +public class RestJsonContentTypeProtocolTest extends ProtocolTestBase { + private static final ProtocolTestSuiteLoader testSuiteLoader = new ProtocolTestSuiteLoader(); + private static ProtocolTestRunner testRunner; + + @Parameterized.Parameter + public TestCase testCase; + + @Parameterized.Parameters(name = "{0}") + public static List data() throws IOException { + return testSuiteLoader.load("restjson-contenttype-suite.json"); + } + + @BeforeClass + public static void setupFixture() { + testRunner = new ProtocolTestRunner("/models/rest-test-2021-05-13-intermediate.json"); + } + + @Test + public void runProtocolTest() throws Exception { + testRunner.runTest(testCase); + } +}