Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@

import static java.util.Collections.unmodifiableList;
import static java.util.Collections.unmodifiableMap;
import static software.amazon.awssdk.enhanced.dynamodb.internal.document.JsonStringFormatHelper.addEscapeCharacters;
import static software.amazon.awssdk.enhanced.dynamodb.internal.document.JsonStringFormatHelper.stringValue;

import java.util.ArrayList;
import java.util.Arrays;
Expand Down Expand Up @@ -193,7 +191,7 @@ public String getJson(String attributeName) {
if (attributeValue == null) {
return null;
}
return stringValue(JSON_ATTRIBUTE_CONVERTER.transformTo(attributeValue));
return DocumentJsonSerializer.serializeSingleAttributeValue(attributeValue);
}

@Override
Expand Down Expand Up @@ -230,12 +228,7 @@ public String toJson() {
if (nonAttributeValueMap.isEmpty()) {
return "{}";
}
return attributeValueMap.getValue().entrySet().stream()
.map(entry -> "\""
+ addEscapeCharacters(entry.getKey())
+ "\":"
+ stringValue(JSON_ATTRIBUTE_CONVERTER.transformTo(entry.getValue())))
.collect(Collectors.joining(",", "{", "}"));
return DocumentJsonSerializer.serializeAttributeValueMap(attributeValueMap.getValue());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* 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.enhanced.dynamodb.internal.document;

import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.core.SdkBytes;
import software.amazon.awssdk.protocols.json.SdkJsonGenerator;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.thirdparty.jackson.core.JsonFactory;
import software.amazon.awssdk.thirdparty.jackson.core.JsonGenerator;

/**
* JSON serializer for DynamoDB Enhanced Client document operations.
*/
@SdkInternalApi
final class DocumentJsonSerializer {
private static final JsonFactory JSON_FACTORY = new JsonFactory() {
@Override
public JsonGenerator createGenerator(OutputStream out) throws IOException {
return super.createGenerator(out).enable(JsonGenerator.Feature.COMBINE_UNICODE_SURROGATES_IN_UTF8);
}
};

private DocumentJsonSerializer() {
}

public static String serializeAttributeValueMap(Map<String, AttributeValue> map) {
SdkJsonGenerator jsonGen = new SdkJsonGenerator(JSON_FACTORY, "application/json");

jsonGen.writeStartObject();
map.forEach((key, value) -> {
jsonGen.writeFieldName(key);
serializeAttributeValue(jsonGen, value);
});
jsonGen.writeEndObject();

return new String(jsonGen.getBytes(), StandardCharsets.UTF_8);
}

public static String serializeSingleAttributeValue(AttributeValue av) {
SdkJsonGenerator jsonGen = new SdkJsonGenerator(JSON_FACTORY, "application/json");
serializeAttributeValue(jsonGen, av);
return new String(jsonGen.getBytes(), StandardCharsets.UTF_8);
}

public static void serializeAttributeValue(SdkJsonGenerator generator, AttributeValue av) {
switch (av.type()) {
case NUL:
generator.writeNull();
break;
case S:
generator.writeValue(av.s());
break;
case N:
generator.writeNumber(av.n());
break;
case BOOL:
generator.writeValue(av.bool());
break;
case B:
generator.writeValue(av.b().asByteBuffer());
break;
case L:
generator.writeStartArray();
for (AttributeValue item : av.l()) {
serializeAttributeValue(generator, item);
}
generator.writeEndArray();
break;
case M:
generator.writeStartObject();
for (Map.Entry<String, AttributeValue> entry : av.m().entrySet()) {
generator.writeFieldName(entry.getKey());
serializeAttributeValue(generator, entry.getValue());
}
generator.writeEndObject();
break;
case SS:
generator.writeStartArray();
for (String s : av.ss()) {
generator.writeValue(s);
}
generator.writeEndArray();
break;
case NS:
generator.writeStartArray();
for (String n : av.ns()) {
generator.writeNumber(n);
}
generator.writeEndArray();
break;
case BS:
generator.writeStartArray();
for (SdkBytes b : av.bs()) {
generator.writeValue(b.asByteBuffer());
}
generator.writeEndArray();
break;
default:
throw new IllegalArgumentException("Unsupported AttributeValue type: " + av.type());
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ArgumentsSource;
import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType;
import software.amazon.awssdk.enhanced.dynamodb.DefaultAttributeConverterProvider;
import software.amazon.awssdk.enhanced.dynamodb.EnhancedType;
import software.amazon.awssdk.enhanced.dynamodb.TableMetadata;
import software.amazon.awssdk.enhanced.dynamodb.converters.document.CustomAttributeForDocumentConverterProvider;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import static software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocumentTestData.testDataInstance;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.math.BigDecimal;
import java.time.LocalDate;
Expand Down Expand Up @@ -52,6 +53,8 @@

class EnhancedDocumentTest {

ObjectMapper mapper = new ObjectMapper();

private static Stream<Arguments> escapeDocumentStrings() {
char c = 0x0a;
return Stream.of(
Expand Down Expand Up @@ -496,4 +499,184 @@ void error_when_usingClassGetPut_for_CollectionValues(){
() -> EnhancedDocument.builder().build().get("listKey" , List.class))
.withMessage("Values of type List are not supported by this API, please use the getList API instead");
}

@Test
void toJson_numberFormatting_veryLargeNumbers() throws JsonProcessingException {
EnhancedDocument doc = EnhancedDocument.builder()
.attributeConverterProviders(defaultProvider())
.putNumber("longMax", Long.MAX_VALUE)
.putNumber("longMin", Long.MIN_VALUE)
.putNumber("doubleMax", Double.MAX_VALUE)
.putNumber("scientific", 1.23e+100)
.putNumber("manyDecimals", 1.123456789012345)
.build();

String json = doc.toJson();
JsonNode actual = mapper.readTree(json);
JsonNode expected = mapper.readTree("{\"longMax\":9223372036854775807,\"longMin\":-9223372036854775808,"
+ "\"doubleMax\":1.7976931348623157E308,\"scientific\":1.23E100,"
+ "\"manyDecimals\":1.123456789012345}");
assertThat(expected).isEqualTo(actual);
}

@Test
void toJson_numberFormatting_trailingZeros() throws JsonProcessingException {
EnhancedDocument doc = EnhancedDocument.builder()
.attributeConverterProviders(defaultProvider())
.putNumber("twoPointZero", 2.0)
.putNumber("tenPointZeroZero", 10.00)
.build();

String json = doc.toJson();
JsonNode actual = mapper.readTree(json);
JsonNode expected = mapper.readTree("{\"twoPointZero\":2.0,\"tenPointZeroZero\":10.0}");
assertThat(expected).isEqualTo(actual);
}

@Test
void toJson_stringEscaping_allControlCharacters() throws JsonProcessingException {
EnhancedDocument doc = EnhancedDocument.builder()
.attributeConverterProviders(defaultProvider())
.putString("allEscapes", "line1\nline2\ttab\"quote\\backslash\r\f")
.build();

String json = doc.toJson();
JsonNode actual = mapper.readTree(json);
JsonNode expected = mapper.readTree("{\"allEscapes\":\"line1\\nline2\\ttab\\\"quote\\\\backslash\\r\\f\"}");
assertThat(expected).isEqualTo(actual);
}

@Test
void toJson_stringEscaping_forwardSlash() throws JsonProcessingException {
EnhancedDocument doc = EnhancedDocument.builder()
.attributeConverterProviders(defaultProvider())
.putString("slash", "path/to/resource")
.build();

String json = doc.toJson();
JsonNode actual = mapper.readTree(json);
JsonNode expected = mapper.readTree("{\"slash\":\"path/to/resource\"}");
assertThat(expected).isEqualTo(actual);
}

@Test
void toJson_emptyString() throws JsonProcessingException {
EnhancedDocument doc = EnhancedDocument.builder()
.attributeConverterProviders(defaultProvider())
.putString("empty", "")
.build();

String json = doc.toJson();
JsonNode actual = mapper.readTree(json);
JsonNode expected = mapper.readTree("{\"empty\":\"\"}");
assertThat(expected).isEqualTo(actual);
}

@Test
void toJson_bytesEncoding_emptyBytes() throws JsonProcessingException {
EnhancedDocument doc = EnhancedDocument.builder()
.attributeConverterProviders(defaultProvider())
.putBytes("empty", SdkBytes.fromByteArray(new byte[0]))
.build();

String json = doc.toJson();
JsonNode actual = mapper.readTree(json);
JsonNode expected = mapper.readTree("{\"empty\":\"\"}");
assertThat(expected).isEqualTo(actual);
}

@Test
void toJson_listWithAllNulls() throws JsonProcessingException {
EnhancedDocument doc = EnhancedDocument.builder()
.attributeConverterProviders(defaultProvider())
.putJson("nullList", "[null,null,null]")
.build();

String json = doc.toJson();
JsonNode actual = mapper.readTree(json);
JsonNode expected = mapper.readTree("{\"nullList\":[null,null,null]}");
assertThat(expected).isEqualTo(actual);
}

@Test
void toJson_mapWithNullValues() throws JsonProcessingException {
EnhancedDocument doc = EnhancedDocument.builder()
.attributeConverterProviders(defaultProvider())
.putJson("nullValues", "{\"key1\":null,\"key2\":\"value\",\"key3\":null}")
.build();

String json = doc.toJson();
JsonNode actual = mapper.readTree(json);
JsonNode expected = mapper.readTree("{\"nullValues\":{\"key1\":null,\"key2\":\"value\",\"key3\":null}}");
assertThat(expected).isEqualTo(actual);
}

@Test
void toJson_deeplyNestedStructure() throws JsonProcessingException {
String deepJson = "{\"level1\":{\"level2\":{\"level3\":{\"level4\":"
+ "{\"level5\":{\"level6\":{\"level7\":{\"value\":\"deep\"}}}}}}}}";

EnhancedDocument doc = EnhancedDocument.builder()
.attributeConverterProviders(defaultProvider())
.putJson("nested", deepJson)
.build();

String json = doc.toJson();

JsonNode actual = mapper.readTree(json);
JsonNode expected = mapper.readTree("{\"nested\":" + deepJson + "}");
assertThat(expected).isEqualTo(actual);
}

@Test
void toJson_deeplyNestedArrays() throws JsonProcessingException {
String deepArrayJson = "[[[[[[\"innermost\"]]]]]]";

EnhancedDocument doc = EnhancedDocument.builder()
.attributeConverterProviders(defaultProvider())
.putJson("nestedArrays", deepArrayJson)
.build();

String json = doc.toJson();
JsonNode actual = mapper.readTree(json);
JsonNode expected = mapper.readTree("{\"nestedArrays\":" + deepArrayJson + "}");
assertThat(expected).isEqualTo(actual);
}

@Test
void toJson_emoji() {

String emoji = "{\"smile\":\"Hello 😀 World\"}";

EnhancedDocument doc = EnhancedDocument.builder()
.attributeConverterProviders(defaultProvider())
.putJson("emoji", emoji)
.build();

String json = doc.toJson();
assertThat(json).isEqualTo("{\"emoji\":" + emoji + "}");
}

@Test
void getJson_returnsRawValue() {
EnhancedDocument doc = EnhancedDocument.builder()
.attributeConverterProviders(defaultProvider())
.putString("key", "value")
.build();

String result = doc.getJson("key");
assertThat(result).isEqualTo("\"value\"");
}

@Test
void getJson_nonExistentAttribute() {
EnhancedDocument doc = EnhancedDocument.builder()
.attributeConverterProviders(defaultProvider())
.putString("exists", "value")
.build();

String result = doc.getJson("doesNotExist");
assertThat(result).isNull();
}

}
Loading
Loading