From 4a08705ad01b020a1769c0673318969fd65e7252 Mon Sep 17 00:00:00 2001 From: Hui Zhao Date: Mon, 20 Mar 2023 15:19:07 -0700 Subject: [PATCH] fix(datastore): SerializedCustomType de/serialization --- .../appsync/SerializedCustomTypeAdapter.java | 75 +++-- .../appsync/SerializedModelAdapter.java | 48 +++- .../SerializedCustomTypeAdapterTest.java | 264 ++++++++++++++++-- .../appsync/SerializedModelAdapterTest.java | 125 +++++++++ ...om-type-se-deserialization-null-field.json | 180 ++++++++++++ ...alized-custom-type-se-deserialization.json | 242 ++++++++++++++-- ...alized-custom-type-se-deserialization.json | 54 +++- ...nested-custom-type-se-deserialization.json | 112 ++++++++ .../appsync/AppSyncRequestFactory.java | 13 +- .../sqlite/SQLiteModelFieldTypeConverter.java | 9 + .../core/model/SerializedCustomType.java | 43 +++ .../core/model/CustomTypeSchemaTest.java | 203 ++++++++++++++ .../serialized-custom-type-nested-data.json | 27 ++ 13 files changed, 1287 insertions(+), 108 deletions(-) create mode 100644 aws-api-appsync/src/test/resources/nested-serialized-custom-type-se-deserialization-null-field.json create mode 100644 aws-api-appsync/src/test/resources/serialized-model-with-nested-custom-type-se-deserialization.json create mode 100644 core/src/test/resources/serialized-custom-type-nested-data.json diff --git a/aws-api-appsync/src/main/java/com/amplifyframework/datastore/appsync/SerializedCustomTypeAdapter.java b/aws-api-appsync/src/main/java/com/amplifyframework/datastore/appsync/SerializedCustomTypeAdapter.java index a57d91640b..8272c6947e 100644 --- a/aws-api-appsync/src/main/java/com/amplifyframework/datastore/appsync/SerializedCustomTypeAdapter.java +++ b/aws-api-appsync/src/main/java/com/amplifyframework/datastore/appsync/SerializedCustomTypeAdapter.java @@ -15,6 +15,8 @@ package com.amplifyframework.datastore.appsync; +import com.amplifyframework.core.model.CustomTypeField; +import com.amplifyframework.core.model.CustomTypeSchema; import com.amplifyframework.core.model.SerializedCustomType; import com.amplifyframework.util.GsonObjectConverter; @@ -31,7 +33,6 @@ import java.lang.reflect.Type; import java.util.ArrayList; import java.util.HashMap; -import java.util.List; import java.util.Map; /** @@ -53,50 +54,66 @@ public static void register(GsonBuilder builder) { @Override public JsonElement serialize(SerializedCustomType src, Type typeOfSrc, JsonSerializationContext context) { - return context.serialize(src.getSerializedData()); + CustomTypeSchema schema = src.getCustomTypeSchema(); + JsonObject result = new JsonObject(); + result.add("customTypeSchema", context.serialize(schema)); + + JsonObject serializedData = new JsonObject(); + + for (Map.Entry entry : src.getSerializedData().entrySet()) { + Object fieldValue = entry.getValue(); + if (fieldValue instanceof SerializedCustomType) { + // serialize by type SerializedCustomType + serializedData.add(entry.getKey(), context.serialize((SerializedCustomType) fieldValue)); + } else { + serializedData.add(entry.getKey(), context.serialize(fieldValue)); + } + } + + result.add("serializedData", serializedData); + return result; } @Override public SerializedCustomType deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject serializedDataObject = json.getAsJsonObject(); + JsonObject object = json.getAsJsonObject(); + CustomTypeSchema schema = context.deserialize( + object.get("customTypeSchema"), CustomTypeSchema.class); + + JsonObject serializedDataObject = object.get("serializedData").getAsJsonObject(); Map serializedData = new HashMap<>(GsonObjectConverter.toMap(serializedDataObject)); // Patch up nested models as SerializedCustomTypes themselves. for (Map.Entry entry : serializedDataObject.entrySet()) { - if (entry.getValue().isJsonObject()) { - serializedData.put(entry.getKey(), SerializedCustomType.builder() - .serializedData(deserialize(entry.getValue(), typeOfT, context).getSerializedData()) - .customTypeSchema(null) - .build()); - } else if (entry.getValue().isJsonArray()) { - JsonArray arrayList = entry.getValue().getAsJsonArray(); - ArrayList nestedList = new ArrayList<>(); - for (int i = 0; i < arrayList.size(); i++) { - JsonElement item = arrayList.get(i); - if (item.isJsonObject()) { - nestedList.add(SerializedCustomType.builder() - .serializedData(deserialize(item, typeOfT, context).getSerializedData()) - .customTypeSchema(null) - .build()); - } else { - @SuppressWarnings("unchecked") - List serializedList = (List) serializedData.get(entry.getKey()); - - if (serializedList != null) { - nestedList.add(serializedList.get(i)); - } + CustomTypeField field = schema.getFields().get(entry.getKey()); + if (field == null) { + continue; + } + + JsonElement fieldValue = entry.getValue(); + String fieldName = field.getName(); + + if (field.isCustomType()) { + if (!field.isArray() && fieldValue.isJsonObject()) { + serializedData.put( + fieldName, context.deserialize(fieldValue, SerializedCustomType.class)); + } else if (field.isArray() && fieldValue.isJsonArray()) { + JsonArray arrayList = fieldValue.getAsJsonArray(); + ArrayList nestedList = new ArrayList<>(); + for (int i = 0; i < arrayList.size(); i++) { + JsonElement item = arrayList.get(i); + nestedList.add(context.deserialize(item, SerializedCustomType.class)); } + + serializedData.put(fieldName, nestedList); } - serializedData.put(entry.getKey(), nestedList); - } else { - serializedData.put(entry.getKey(), serializedData.get(entry.getKey())); } } return SerializedCustomType.builder() .serializedData(serializedData) - .customTypeSchema(null) + .customTypeSchema(schema) .build(); } } diff --git a/aws-api-appsync/src/main/java/com/amplifyframework/datastore/appsync/SerializedModelAdapter.java b/aws-api-appsync/src/main/java/com/amplifyframework/datastore/appsync/SerializedModelAdapter.java index 676728ea35..9400baa60c 100644 --- a/aws-api-appsync/src/main/java/com/amplifyframework/datastore/appsync/SerializedModelAdapter.java +++ b/aws-api-appsync/src/main/java/com/amplifyframework/datastore/appsync/SerializedModelAdapter.java @@ -18,6 +18,7 @@ import com.amplifyframework.core.model.ModelField; import com.amplifyframework.core.model.ModelSchema; import com.amplifyframework.core.model.SchemaRegistry; +import com.amplifyframework.core.model.SerializedCustomType; import com.amplifyframework.core.model.SerializedModel; import com.amplifyframework.util.GsonObjectConverter; @@ -41,10 +42,12 @@ */ public final class SerializedModelAdapter implements JsonDeserializer, JsonSerializer { - private SerializedModelAdapter() {} + private SerializedModelAdapter() { + } /** * Registers an adapter with a Gson builder. + * * @param builder A gson builder */ public static void register(GsonBuilder builder) { @@ -61,11 +64,16 @@ public JsonElement serialize(SerializedModel src, Type typeOfSrc, JsonSerializat JsonObject serializedData = new JsonObject(); for (Map.Entry entry : src.getSerializedData().entrySet()) { - if (entry.getValue() instanceof SerializedModel) { - SerializedModel serializedModel = (SerializedModel) entry.getValue(); - serializedData.add(entry.getKey(), context.serialize(serializedModel.getSerializedData())); + String fieldName = entry.getKey(); + Object fieldValue = entry.getValue(); + if (fieldValue instanceof SerializedModel) { + SerializedModel serializedModel = (SerializedModel) fieldValue; + serializedData.add(fieldName, context.serialize(serializedModel.getSerializedData())); + } else if (fieldValue instanceof SerializedCustomType) { + // serialize via SerializedCustomTypeAdapter + serializedData.add(fieldName, context.serialize(fieldValue)); } else { - serializedData.add(entry.getKey(), context.serialize(entry.getValue())); + serializedData.add(fieldName, context.serialize(fieldValue)); } } result.add("serializedData", serializedData); @@ -84,21 +92,33 @@ public SerializedModel deserialize(JsonElement json, Type typeOfT, JsonDeseriali // Patch up nested models as SerializedModels themselves. for (Map.Entry item : serializedDataObject.entrySet()) { ModelField field = modelSchema.getFields().get(item.getKey()); - if (field != null && field.isModel()) { + if (field == null) { + continue; + } + + JsonElement fieldValue = item.getValue(); + String fieldName = field.getName(); + + // if the field type is a Model - convert the nested data into SerializedModel + if (field.isModel()) { SchemaRegistry schemaRegistry = SchemaRegistry.instance(); ModelSchema nestedModelSchema = schemaRegistry.getModelSchemaForModelClass(field.getTargetType()); Gson gson = new Gson(); - Type mapType = new TypeToken>() {}.getType(); - serializedData.put(field.getName(), SerializedModel.builder() - .modelSchema(nestedModelSchema) - .serializedData(gson.fromJson(item.getValue(), mapType)) - .build()); + Type mapType = new TypeToken>() { + }.getType(); + serializedData.put(fieldName, SerializedModel.builder() + .modelSchema(nestedModelSchema) + .serializedData(gson.fromJson(fieldValue, mapType)) + .build()); + } else if (field.isCustomType()) { + // if the field type is a CustomType - convert the nested data into SerializedCustomType + serializedData.put(fieldName, context.deserialize(fieldValue, SerializedCustomType.class)); } } return SerializedModel.builder() - .modelSchema(modelSchema) - .serializedData(serializedData) - .build(); + .modelSchema(modelSchema) + .serializedData(serializedData) + .build(); } } diff --git a/aws-api-appsync/src/test/java/com/amplifyframework/datastore/appsync/SerializedCustomTypeAdapterTest.java b/aws-api-appsync/src/test/java/com/amplifyframework/datastore/appsync/SerializedCustomTypeAdapterTest.java index 10f3da5e6f..030b2a42a5 100644 --- a/aws-api-appsync/src/test/java/com/amplifyframework/datastore/appsync/SerializedCustomTypeAdapterTest.java +++ b/aws-api-appsync/src/test/java/com/amplifyframework/datastore/appsync/SerializedCustomTypeAdapterTest.java @@ -15,6 +15,8 @@ package com.amplifyframework.datastore.appsync; +import com.amplifyframework.core.model.CustomTypeField; +import com.amplifyframework.core.model.CustomTypeSchema; import com.amplifyframework.core.model.SerializedCustomType; import com.amplifyframework.core.model.types.GsonJavaTypeAdapters; import com.amplifyframework.testutils.Resources; @@ -30,6 +32,7 @@ import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -37,9 +40,13 @@ */ public final class SerializedCustomTypeAdapterTest { private Gson gson; + private CustomTypeSchema addressSchema; + private CustomTypeSchema contactSchema; + private CustomTypeSchema phoneSchema; + private CustomTypeSchema personInfoSchema; /** - * Register some models. + * Set up Gson adapters and create testing custom type schema. */ @Before public void setup() { @@ -47,6 +54,36 @@ public void setup() { GsonJavaTypeAdapters.register(builder); SerializedCustomTypeAdapter.register(builder); gson = builder.create(); + + /* + # Testing schema: + # All custom types (non-models) + type Person { + name: String! + mailingAddresses: [Address] + contact: Contact + tags: [String] + } + + type Address { + line1: String! + line2: String + postalCode: String! + state: String! + } + + type Phone { + countryCode: String! + areaCode: String! + number: String! + } + + type Contact { + phone: Phone + email: String + } + */ + createCustomTypeSchema(); } /** @@ -62,18 +99,18 @@ public void simpleSerializedCustomTypeSerializeSerializationAndDeserialization() serializedData.put("state", "CA"); serializedData.put("postalCode", "123456"); - SerializedCustomType serializedCustomType = SerializedCustomType.builder() + SerializedCustomType serializedAddressCustomType = SerializedCustomType.builder() .serializedData(serializedData) - .customTypeSchema(null) + .customTypeSchema(addressSchema) .build(); String expectedResourcePath = "serialized-custom-type-se-deserialization.json"; String expectedJson = Resources.readAsJson(expectedResourcePath).toString(2); - String actualJson = new JSONObject(gson.toJson(serializedCustomType)).toString(2); + String actualJson = new JSONObject(gson.toJson(serializedAddressCustomType)).toString(2); JSONAssert.assertEquals(expectedJson, actualJson, true); SerializedCustomType recovered = gson.fromJson(expectedJson, SerializedCustomType.class); - Assert.assertEquals(serializedCustomType, recovered); + Assert.assertEquals(serializedAddressCustomType, recovered); } /** @@ -82,7 +119,7 @@ public void simpleSerializedCustomTypeSerializeSerializationAndDeserialization() * @throws JSONException On illegal json found by JSONAssert */ @Test - public void nestedSerializedCustomTypeSerializeSerializationAndDeserialization() throws JSONException { + public void nestedSerializedCustomType() throws JSONException { Map addressSerializedData1 = new HashMap<>(); addressSerializedData1.put("line1", "222 Somewhere far"); addressSerializedData1.put("line2", null); @@ -91,7 +128,7 @@ public void nestedSerializedCustomTypeSerializeSerializationAndDeserialization() SerializedCustomType address1 = SerializedCustomType.builder() .serializedData(addressSerializedData1) - .customTypeSchema(null) + .customTypeSchema(addressSchema) .build(); Map addressSerializedData2 = new HashMap<>(); @@ -102,7 +139,7 @@ public void nestedSerializedCustomTypeSerializeSerializationAndDeserialization() SerializedCustomType address2 = SerializedCustomType.builder() .serializedData(addressSerializedData2) - .customTypeSchema(null) + .customTypeSchema(addressSchema) .build(); ArrayList addresses = new ArrayList<>(); @@ -116,7 +153,7 @@ public void nestedSerializedCustomTypeSerializeSerializationAndDeserialization() SerializedCustomType phone = SerializedCustomType.builder() .serializedData(phoneSerializedData) - .customTypeSchema(null) + .customTypeSchema(phoneSchema) .build(); Map contactSerializedData = new HashMap<>(); @@ -125,22 +162,22 @@ public void nestedSerializedCustomTypeSerializeSerializationAndDeserialization() SerializedCustomType contact = SerializedCustomType.builder() .serializedData(contactSerializedData) - .customTypeSchema(null) + .customTypeSchema(contactSchema) .build(); - ArrayList stringList = new ArrayList<>(); - stringList.add("string1"); - stringList.add("string2"); + ArrayList tags = new ArrayList<>(); + tags.add("string1"); + tags.add("string2"); Map personalSerializedData = new HashMap<>(); personalSerializedData.put("name", "Tester Testing"); personalSerializedData.put("mailingAddresses", addresses); personalSerializedData.put("contact", contact); - personalSerializedData.put("arrayList", stringList); + personalSerializedData.put("tags", tags); SerializedCustomType person = SerializedCustomType.builder() .serializedData(personalSerializedData) - .customTypeSchema(null) + .customTypeSchema(personInfoSchema) .build(); String expectedResourcePath = "nested-serialized-custom-type-se-deserialization.json"; @@ -153,17 +190,198 @@ public void nestedSerializedCustomTypeSerializeSerializationAndDeserialization() } /** - * Test Nested SerializedCustomType serialization and deserialization. + * Test Nested SerializedCustomType with nullable field having null value serialization and deserialization. * * @throws JSONException On illegal json found by JSONAssert */ @Test - public void serializedCustomTypeNestsOtherTypes() { - Map bioSerializedData = new HashMap<>(); - bioSerializedData.put("name", "Someone Testing"); - bioSerializedData.put("birthday", "2020-11-05Z"); - bioSerializedData.put("dateTime", "2020-11-05T03:44:28Z"); - bioSerializedData.put("time", "03:44:28Z"); - bioSerializedData.put("timestamp", 1604547868); + public void nestedSerializedCustomTypeWithNullableField() throws JSONException { + Map addressSerializedData1 = new HashMap<>(); + addressSerializedData1.put("line1", "222 Somewhere far"); + addressSerializedData1.put("line2", null); + addressSerializedData1.put("state", "CA"); + addressSerializedData1.put("postalCode", "123456"); + + SerializedCustomType address1 = SerializedCustomType.builder() + .serializedData(addressSerializedData1) + .customTypeSchema(addressSchema) + .build(); + + Map addressSerializedData2 = new HashMap<>(); + addressSerializedData2.put("line1", "444 Somewhere close"); + addressSerializedData2.put("line2", "Apt 3"); + addressSerializedData2.put("state", "WA"); + addressSerializedData2.put("postalCode", "123456"); + + SerializedCustomType address2 = SerializedCustomType.builder() + .serializedData(addressSerializedData2) + .customTypeSchema(addressSchema) + .build(); + + ArrayList addresses = new ArrayList<>(); + addresses.add(address1); + addresses.add(address2); + + Map phoneSerializedData = new HashMap<>(); + phoneSerializedData.put("countryCode", "1"); + phoneSerializedData.put("areaCode", "415"); + phoneSerializedData.put("phone", "6666666"); + + SerializedCustomType phone = SerializedCustomType.builder() + .serializedData(phoneSerializedData) + .customTypeSchema(phoneSchema) + .build(); + + Map contactSerializedData = new HashMap<>(); + contactSerializedData.put("email", "tester@testing.com"); + contactSerializedData.put("phone", null); + + SerializedCustomType contact = SerializedCustomType.builder() + .serializedData(contactSerializedData) + .customTypeSchema(contactSchema) + .build(); + + ArrayList tags = new ArrayList<>(); + tags.add("string1"); + tags.add("string2"); + + Map personalSerializedData = new HashMap<>(); + personalSerializedData.put("name", "Tester Testing"); + personalSerializedData.put("mailingAddresses", addresses); + personalSerializedData.put("contact", contact); + personalSerializedData.put("tags", tags); + + SerializedCustomType person = SerializedCustomType.builder() + .serializedData(personalSerializedData) + .customTypeSchema(personInfoSchema) + .build(); + + String expectedResourcePath = "nested-serialized-custom-type-se-deserialization-null-field.json"; + String expectedJson = Resources.readAsJson(expectedResourcePath).toString(2); + String actualJson = new JSONObject(gson.toJson(person)).toString(2); + JSONAssert.assertEquals(expectedJson, actualJson, true); + + SerializedCustomType recovered = gson.fromJson(expectedJson, SerializedCustomType.class); + Assert.assertEquals(person, recovered); + } + + private void createCustomTypeSchema() { + Map phoneFields = new HashMap<>(); + phoneFields.put( + "areaCode", CustomTypeField.builder() + .name("areaCode") + .javaClassForValue(String.class) + .targetType("String") + .isRequired(true) + .build()); + phoneFields.put( + "number", CustomTypeField.builder() + .name("number") + .javaClassForValue(String.class) + .targetType("String") + .isRequired(true) + .build()); + phoneFields.put( + "countryCode", CustomTypeField.builder() + .name("countryCode") + .javaClassForValue(String.class) + .targetType("String") + .isRequired(true) + .build()); + phoneSchema = CustomTypeSchema.builder() + .name("Phone") + .pluralName("Phones") + .fields(phoneFields) + .build(); + + Map contactFields = new HashMap<>(); + contactFields.put( + "phone", CustomTypeField.builder() + .name("phone") + .javaClassForValue(Map.class) + .targetType("Phone") + .isCustomType(true) + .build()); + contactFields.put( + "email", CustomTypeField.builder() + .name("email") + .javaClassForValue(String.class) + .targetType("String") + .build()); + contactSchema = CustomTypeSchema.builder() + .name("Contact") + .pluralName("Contacts") + .fields(contactFields) + .build(); + + Map addressFields = new HashMap<>(); + addressFields.put( + "postalCode", CustomTypeField.builder() + .name("postalCode") + .javaClassForValue(String.class) + .targetType("String") + .isRequired(true) + .build()); + addressFields.put( + "line1", CustomTypeField.builder() + .name("line1") + .javaClassForValue(String.class) + .targetType("String") + .isRequired(true) + .build()); + addressFields.put( + "line2", CustomTypeField.builder() + .name("line2") + .javaClassForValue(String.class) + .targetType("String") + .build()); + addressFields.put( + "state", CustomTypeField.builder() + .name("state") + .javaClassForValue(String.class) + .targetType("String") + .isRequired(true) + .build()); + addressSchema = CustomTypeSchema.builder() + .name("Address") + .pluralName("Addresses") + .fields(addressFields) + .build(); + + Map personInfoFields = new HashMap<>(); + personInfoFields.put( + "name", CustomTypeField.builder() + .name("name") + .javaClassForValue(String.class) + .targetType("String") + .isRequired(true) + .build()); + personInfoFields.put( + "mailingAddresses", CustomTypeField.builder() + .name("mailingAddresses") + .javaClassForValue(List.class) + .targetType("Address") + .isCustomType(true) + .isArray(true) + .build()); + personInfoFields.put( + "contact", CustomTypeField.builder() + .name("contact") + .javaClassForValue(Map.class) + .targetType("Contact") + .isCustomType(true) + .build()); + personInfoFields.put( + "tags", CustomTypeField.builder() + .name("tags") + .isArray(true) + .javaClassForValue(List.class) + .targetType("String") + .build()); + personInfoSchema = CustomTypeSchema.builder() + .name("Person") + .pluralName("People") + .fields(personInfoFields) + .build(); } } diff --git a/aws-api-appsync/src/test/java/com/amplifyframework/datastore/appsync/SerializedModelAdapterTest.java b/aws-api-appsync/src/test/java/com/amplifyframework/datastore/appsync/SerializedModelAdapterTest.java index b756419bf8..9fb39aa6d8 100644 --- a/aws-api-appsync/src/test/java/com/amplifyframework/datastore/appsync/SerializedModelAdapterTest.java +++ b/aws-api-appsync/src/test/java/com/amplifyframework/datastore/appsync/SerializedModelAdapterTest.java @@ -16,10 +16,13 @@ package com.amplifyframework.datastore.appsync; import com.amplifyframework.AmplifyException; +import com.amplifyframework.core.model.CustomTypeField; +import com.amplifyframework.core.model.CustomTypeSchema; import com.amplifyframework.core.model.ModelAssociation; import com.amplifyframework.core.model.ModelField; import com.amplifyframework.core.model.ModelSchema; import com.amplifyframework.core.model.SchemaRegistry; +import com.amplifyframework.core.model.SerializedCustomType; import com.amplifyframework.core.model.SerializedModel; import com.amplifyframework.core.model.temporal.GsonTemporalAdapters; import com.amplifyframework.core.model.temporal.Temporal; @@ -59,6 +62,7 @@ public void setup() { GsonJavaTypeAdapters.register(builder); GsonTemporalAdapters.register(builder); SerializedModelAdapter.register(builder); + SerializedCustomTypeAdapter.register(builder); gson = builder.create(); } @@ -127,6 +131,56 @@ public void serdeForNestedSerializedModels() throws JSONException, AmplifyExcept Assert.assertEquals(blogAsSerializedModel, recovered); } + /** + * Tests serialization and deserialization of a model that contains nested custom type. + * + * @throws JSONException On illegal json found by JSONAssert + * @throws AmplifyException On unable to parse schema + */ + @Test + public void serdeForNestedCustomTypes() throws JSONException, AmplifyException { + CustomTypeSchema phoneSchema = customTypeSchemaForPhone(); + CustomTypeSchema contactSchema = customTypeSchemaForContact(); + ModelSchema personSchema = modelSchemaForPerson(); + + SchemaRegistry schemaRegistry = SchemaRegistry.instance(); + schemaRegistry.register("Person", personSchema); + + Map phoneSerializedData = new HashMap<>(); + phoneSerializedData.put("countryCode", "+1"); + phoneSerializedData.put("number", "41555555555"); + SerializedCustomType phone = SerializedCustomType.builder() + .serializedData(phoneSerializedData) + .customTypeSchema(phoneSchema) + .build(); + + Map contactSerializedData = new HashMap<>(); + contactSerializedData.put("email", "test@test.com"); + contactSerializedData.put("phone", phone); + SerializedCustomType contact = SerializedCustomType.builder() + .serializedData(contactSerializedData) + .customTypeSchema(contactSchema) + .build(); + + Map personSerializedData = new HashMap<>(); + personSerializedData.put("fullName", "Tester Test"); + personSerializedData.put("contact", contact); + personSerializedData.put("id", "some-unique-id"); + SerializedModel person = SerializedModel.builder() + .modelSchema(personSchema) + .serializedData(personSerializedData) + .build(); + + String resourcePath = "serialized-model-with-nested-custom-type-se-deserialization.json"; + String expectedJson = new JSONObject(Resources.readAsString(resourcePath)).toString(2); + String actualJson = new JSONObject(gson.toJson(person)).toString(2); + + Assert.assertEquals(expectedJson, actualJson); + + SerializedModel recovered = gson.fromJson(expectedJson, SerializedModel.class); + Assert.assertEquals(person, recovered); + } + private ModelSchema modelSchemaForMeeting() { Map fields = new HashMap<>(); fields.put("id", ModelField.builder() @@ -223,4 +277,75 @@ private ModelSchema modelSchemaForBlog() { .modelClass(SerializedModel.class) .build(); } + + private CustomTypeSchema customTypeSchemaForPhone() { + Map phoneFields = new HashMap<>(); + phoneFields.put("countryCode", CustomTypeField.builder() + .name("countryCode") + .targetType("String") + .javaClassForValue(String.class) + .isRequired(true) + .build()); + phoneFields.put("number", CustomTypeField.builder() + .name("number") + .targetType("String") + .javaClassForValue(String.class) + .isRequired(true) + .build()); + return CustomTypeSchema.builder() + .name("Phone") + .pluralName("Phones") + .fields(phoneFields) + .build(); + } + + private CustomTypeSchema customTypeSchemaForContact() { + Map contactFields = new HashMap<>(); + contactFields.put("email", CustomTypeField.builder() + .name("email") + .targetType("String") + .javaClassForValue(String.class) + .isRequired(true) + .build()); + contactFields.put("phone", CustomTypeField.builder() + .name("phone") + .targetType("Phone") + .javaClassForValue(Map.class) + .isCustomType(true) + .isRequired(true) + .build()); + return CustomTypeSchema.builder() + .name("Contact") + .pluralName("Contacts") + .fields(contactFields) + .build(); + } + + private ModelSchema modelSchemaForPerson() { + Map personFields = new HashMap<>(); + personFields.put("id", ModelField.builder() + .name("id") + .javaClassForValue(String.class) + .targetType("ID") + .isRequired(true) + .build()); + personFields.put("fullName", ModelField.builder() + .name("fullName") + .targetType("String") + .javaClassForValue(String.class) + .isRequired(true) + .build()); + personFields.put("contact", ModelField.builder() + .name("contact") + .targetType("Contact") + .javaClassForValue(Map.class) + .isRequired(true) + .isCustomType(true) + .build()); + return ModelSchema.builder() + .name("Person") + .pluralName("People") + .fields(personFields) + .build(); + } } diff --git a/aws-api-appsync/src/test/resources/nested-serialized-custom-type-se-deserialization-null-field.json b/aws-api-appsync/src/test/resources/nested-serialized-custom-type-se-deserialization-null-field.json new file mode 100644 index 0000000000..f042502bc9 --- /dev/null +++ b/aws-api-appsync/src/test/resources/nested-serialized-custom-type-se-deserialization-null-field.json @@ -0,0 +1,180 @@ +{ + "serializedData": { + "contact": { + "serializedData": { + "phone": null, + "email": "tester@testing.com" + }, + "customTypeSchema": { + "pluralName": "Contacts", + "name": "Contact", + "fields": { + "phone": { + "javaClassForValue": "java.util.Map", + "isRequired": false, + "isCustomType": true, + "name": "phone", + "isEnum": false, + "targetType": "Phone", + "isArray": false + }, + "email": { + "javaClassForValue": "java.lang.String", + "isRequired": false, + "isCustomType": false, + "name": "email", + "isEnum": false, + "targetType": "String", + "isArray": false + } + } + } + }, + "name": "Tester Testing", + "mailingAddresses": [ + { + "serializedData": { + "postalCode": "123456", + "state": "CA", + "line2": null, + "line1": "222 Somewhere far" + }, + "customTypeSchema": { + "pluralName": "Addresses", + "name": "Address", + "fields": { + "postalCode": { + "javaClassForValue": "java.lang.String", + "isRequired": true, + "isCustomType": false, + "name": "postalCode", + "isEnum": false, + "targetType": "String", + "isArray": false + }, + "state": { + "javaClassForValue": "java.lang.String", + "isRequired": true, + "isCustomType": false, + "name": "state", + "isEnum": false, + "targetType": "String", + "isArray": false + }, + "line2": { + "javaClassForValue": "java.lang.String", + "isRequired": false, + "isCustomType": false, + "name": "line2", + "isEnum": false, + "targetType": "String", + "isArray": false + }, + "line1": { + "javaClassForValue": "java.lang.String", + "isRequired": true, + "isCustomType": false, + "name": "line1", + "isEnum": false, + "targetType": "String", + "isArray": false + } + } + } + }, + { + "serializedData": { + "postalCode": "123456", + "state": "WA", + "line2": "Apt 3", + "line1": "444 Somewhere close" + }, + "customTypeSchema": { + "pluralName": "Addresses", + "name": "Address", + "fields": { + "postalCode": { + "javaClassForValue": "java.lang.String", + "isRequired": true, + "isCustomType": false, + "name": "postalCode", + "isEnum": false, + "targetType": "String", + "isArray": false + }, + "state": { + "javaClassForValue": "java.lang.String", + "isRequired": true, + "isCustomType": false, + "name": "state", + "isEnum": false, + "targetType": "String", + "isArray": false + }, + "line2": { + "javaClassForValue": "java.lang.String", + "isRequired": false, + "isCustomType": false, + "name": "line2", + "isEnum": false, + "targetType": "String", + "isArray": false + }, + "line1": { + "javaClassForValue": "java.lang.String", + "isRequired": true, + "isCustomType": false, + "name": "line1", + "isEnum": false, + "targetType": "String", + "isArray": false + } + } + } + } + ], + "tags": ["string1", "string2"] + }, + "customTypeSchema": { + "pluralName": "People", + "name": "Person", + "fields": { + "contact": { + "javaClassForValue": "java.util.Map", + "isRequired": false, + "isCustomType": true, + "name": "contact", + "isEnum": false, + "targetType": "Contact", + "isArray": false + }, + "name": { + "javaClassForValue": "java.lang.String", + "isRequired": true, + "isCustomType": false, + "name": "name", + "isEnum": false, + "targetType": "String", + "isArray": false + }, + "mailingAddresses": { + "javaClassForValue": "java.util.List", + "isRequired": false, + "isCustomType": true, + "name": "mailingAddresses", + "isEnum": false, + "targetType": "Address", + "isArray": true + }, + "tags": { + "javaClassForValue": "java.util.List", + "isRequired": false, + "isCustomType": false, + "name": "tags", + "isEnum": false, + "targetType": "String", + "isArray": true + } + } + } +} diff --git a/aws-api-appsync/src/test/resources/nested-serialized-custom-type-se-deserialization.json b/aws-api-appsync/src/test/resources/nested-serialized-custom-type-se-deserialization.json index b9d39a01cd..9d2eb20bbf 100644 --- a/aws-api-appsync/src/test/resources/nested-serialized-custom-type-se-deserialization.json +++ b/aws-api-appsync/src/test/resources/nested-serialized-custom-type-se-deserialization.json @@ -1,29 +1,219 @@ { - "mailingAddresses": [ - { - "postalCode": "123456", - "state": "CA", - "line2": null, - "line1": "222 Somewhere far" + "serializedData": { + "contact": { + "serializedData": { + "phone": { + "serializedData": { + "areaCode": "415", + "phone": "6666666", + "countryCode": "1" + }, + "customTypeSchema": { + "pluralName": "Phones", + "name": "Phone", + "fields": { + "number": { + "javaClassForValue": "java.lang.String", + "isRequired": true, + "isCustomType": false, + "name": "number", + "isEnum": false, + "targetType": "String", + "isArray": false + }, + "areaCode": { + "javaClassForValue": "java.lang.String", + "isRequired": true, + "isCustomType": false, + "name": "areaCode", + "isEnum": false, + "targetType": "String", + "isArray": false + }, + "countryCode": { + "javaClassForValue": "java.lang.String", + "isRequired": true, + "isCustomType": false, + "name": "countryCode", + "isEnum": false, + "targetType": "String", + "isArray": false + } + } + } + }, + "email": "tester@testing.com" + }, + "customTypeSchema": { + "pluralName": "Contacts", + "name": "Contact", + "fields": { + "phone": { + "javaClassForValue": "java.util.Map", + "isRequired": false, + "isCustomType": true, + "name": "phone", + "isEnum": false, + "targetType": "Phone", + "isArray": false + }, + "email": { + "javaClassForValue": "java.lang.String", + "isRequired": false, + "isCustomType": false, + "name": "email", + "isEnum": false, + "targetType": "String", + "isArray": false + } + } + } }, - { - "postalCode": "123456", - "state": "WA", - "line2": "Apt 3", - "line1": "444 Somewhere close" - } - ], - "contact": { - "phone": { - "areaCode": "415", - "phone": "6666666", - "countryCode": "1" - }, - "email": "tester@testing.com" + "name": "Tester Testing", + "mailingAddresses": [ + { + "serializedData": { + "postalCode": "123456", + "state": "CA", + "line2": null, + "line1": "222 Somewhere far" + }, + "customTypeSchema": { + "pluralName": "Addresses", + "name": "Address", + "fields": { + "postalCode": { + "javaClassForValue": "java.lang.String", + "isRequired": true, + "isCustomType": false, + "name": "postalCode", + "isEnum": false, + "targetType": "String", + "isArray": false + }, + "state": { + "javaClassForValue": "java.lang.String", + "isRequired": true, + "isCustomType": false, + "name": "state", + "isEnum": false, + "targetType": "String", + "isArray": false + }, + "line2": { + "javaClassForValue": "java.lang.String", + "isRequired": false, + "isCustomType": false, + "name": "line2", + "isEnum": false, + "targetType": "String", + "isArray": false + }, + "line1": { + "javaClassForValue": "java.lang.String", + "isRequired": true, + "isCustomType": false, + "name": "line1", + "isEnum": false, + "targetType": "String", + "isArray": false + } + } + } + }, + { + "serializedData": { + "postalCode": "123456", + "state": "WA", + "line2": "Apt 3", + "line1": "444 Somewhere close" + }, + "customTypeSchema": { + "pluralName": "Addresses", + "name": "Address", + "fields": { + "postalCode": { + "javaClassForValue": "java.lang.String", + "isRequired": true, + "isCustomType": false, + "name": "postalCode", + "isEnum": false, + "targetType": "String", + "isArray": false + }, + "state": { + "javaClassForValue": "java.lang.String", + "isRequired": true, + "isCustomType": false, + "name": "state", + "isEnum": false, + "targetType": "String", + "isArray": false + }, + "line2": { + "javaClassForValue": "java.lang.String", + "isRequired": false, + "isCustomType": false, + "name": "line2", + "isEnum": false, + "targetType": "String", + "isArray": false + }, + "line1": { + "javaClassForValue": "java.lang.String", + "isRequired": true, + "isCustomType": false, + "name": "line1", + "isEnum": false, + "targetType": "String", + "isArray": false + } + } + } + } + ], + "tags": ["string1", "string2"] }, - "name": "Tester Testing", - "arrayList": [ - "string1", - "string2" - ] -} \ No newline at end of file + "customTypeSchema": { + "pluralName": "People", + "name": "Person", + "fields": { + "contact": { + "javaClassForValue": "java.util.Map", + "isRequired": false, + "isCustomType": true, + "name": "contact", + "isEnum": false, + "targetType": "Contact", + "isArray": false + }, + "name": { + "javaClassForValue": "java.lang.String", + "isRequired": true, + "isCustomType": false, + "name": "name", + "isEnum": false, + "targetType": "String", + "isArray": false + }, + "mailingAddresses": { + "javaClassForValue": "java.util.List", + "isRequired": false, + "isCustomType": true, + "name": "mailingAddresses", + "isEnum": false, + "targetType": "Address", + "isArray": true + }, + "tags": { + "javaClassForValue": "java.util.List", + "isRequired": false, + "isCustomType": false, + "name": "tags", + "isEnum": false, + "targetType": "String", + "isArray": true + } + } + } +} diff --git a/aws-api-appsync/src/test/resources/serialized-custom-type-se-deserialization.json b/aws-api-appsync/src/test/resources/serialized-custom-type-se-deserialization.json index f0d819e46c..2ed38c47e7 100644 --- a/aws-api-appsync/src/test/resources/serialized-custom-type-se-deserialization.json +++ b/aws-api-appsync/src/test/resources/serialized-custom-type-se-deserialization.json @@ -1,6 +1,50 @@ { - "postalCode": "123456", - "state": "CA", - "line1": "222 Somewhere far", - "line2": null -} \ No newline at end of file + "serializedData": { + "postalCode": "123456", + "state": "CA", + "line2": null, + "line1": "222 Somewhere far" + }, + "customTypeSchema": { + "pluralName": "Addresses", + "name": "Address", + "fields": { + "postalCode": { + "javaClassForValue": "java.lang.String", + "isRequired": true, + "isCustomType": false, + "name": "postalCode", + "isEnum": false, + "targetType": "String", + "isArray": false + }, + "state": { + "javaClassForValue": "java.lang.String", + "isRequired": true, + "isCustomType": false, + "name": "state", + "isEnum": false, + "targetType": "String", + "isArray": false + }, + "line2": { + "javaClassForValue": "java.lang.String", + "isRequired": false, + "isCustomType": false, + "name": "line2", + "isEnum": false, + "targetType": "String", + "isArray": false + }, + "line1": { + "javaClassForValue": "java.lang.String", + "isRequired": true, + "isCustomType": false, + "name": "line1", + "isEnum": false, + "targetType": "String", + "isArray": false + } + } + } +} diff --git a/aws-api-appsync/src/test/resources/serialized-model-with-nested-custom-type-se-deserialization.json b/aws-api-appsync/src/test/resources/serialized-model-with-nested-custom-type-se-deserialization.json new file mode 100644 index 0000000000..6c55113882 --- /dev/null +++ b/aws-api-appsync/src/test/resources/serialized-model-with-nested-custom-type-se-deserialization.json @@ -0,0 +1,112 @@ +{ + "serializedData": { + "contact": { + "serializedData": { + "phone": { + "serializedData": { + "number": "41555555555", + "countryCode": "+1" + }, + "customTypeSchema": { + "pluralName": "Phones", + "name": "Phone", + "fields": { + "number": { + "javaClassForValue": "java.lang.String", + "isRequired": true, + "isCustomType": false, + "name": "number", + "isEnum": false, + "targetType": "String", + "isArray": false + }, + "countryCode": { + "javaClassForValue": "java.lang.String", + "isRequired": true, + "isCustomType": false, + "name": "countryCode", + "isEnum": false, + "targetType": "String", + "isArray": false + } + } + } + }, + "email": "test@test.com" + }, + "customTypeSchema": { + "pluralName": "Contacts", + "name": "Contact", + "fields": { + "phone": { + "javaClassForValue": "java.util.Map", + "isRequired": true, + "isCustomType": true, + "name": "phone", + "isEnum": false, + "targetType": "Phone", + "isArray": false + }, + "email": { + "javaClassForValue": "java.lang.String", + "isRequired": true, + "isCustomType": false, + "name": "email", + "isEnum": false, + "targetType": "String", + "isArray": false + } + } + } + }, + "fullName": "Tester Test", + "id": "some-unique-id" + }, + "id": "some-unique-id", + "modelSchema": { + "associations": {}, + "pluralName": "People", + "authRules": [], + "indexes": {}, + "name": "Person", + "modelSchemaVersion": 0, + "fields": { + "contact": { + "javaClassForValue": "java.util.Map", + "isRequired": true, + "isCustomType": true, + "isReadOnly": false, + "isModel": false, + "authRules": [], + "name": "contact", + "isEnum": false, + "targetType": "Contact", + "isArray": false + }, + "fullName": { + "javaClassForValue": "java.lang.String", + "isRequired": true, + "isCustomType": false, + "isReadOnly": false, + "isModel": false, + "authRules": [], + "name": "fullName", + "isEnum": false, + "targetType": "String", + "isArray": false + }, + "id": { + "javaClassForValue": "java.lang.String", + "isRequired": true, + "isCustomType": false, + "isReadOnly": false, + "isModel": false, + "authRules": [], + "name": "id", + "isEnum": false, + "targetType": "ID", + "isArray": false + } + } + } +} diff --git a/aws-datastore/src/main/java/com/amplifyframework/datastore/appsync/AppSyncRequestFactory.java b/aws-datastore/src/main/java/com/amplifyframework/datastore/appsync/AppSyncRequestFactory.java index 6f9819a0f9..24e24b2083 100644 --- a/aws-datastore/src/main/java/com/amplifyframework/datastore/appsync/AppSyncRequestFactory.java +++ b/aws-datastore/src/main/java/com/amplifyframework/datastore/appsync/AppSyncRequestFactory.java @@ -524,16 +524,7 @@ private static Object extractCustomTypeFieldValue(String fieldName, Object custo // If a field is a CustomType, it's value is either a SerializedCustomType // or a List of SerializedCustomType if (customTypeData instanceof SerializedCustomType) { - final Map result = new HashMap<>(); - for (Map.Entry entry : - ((SerializedCustomType) customTypeData).getSerializedData().entrySet()) { - if (entry.getValue() instanceof SerializedCustomType) { - result.put(entry.getKey(), extractCustomTypeFieldValue(entry.getKey(), entry.getValue())); - } else { - result.put(entry.getKey(), entry.getValue()); - } - } - return result; + return ((SerializedCustomType) customTypeData).getFlatSerializedData(); } if (customTypeData instanceof List) { @@ -542,7 +533,7 @@ private static Object extractCustomTypeFieldValue(String fieldName, Object custo List customTypeList = (List) customTypeData; for (Object item : customTypeList) { if (item instanceof SerializedCustomType) { - result.add(extractCustomTypeFieldValue(fieldName, item)); + result.add(((SerializedCustomType) item).getFlatSerializedData()); } else { result.add(item); } diff --git a/aws-datastore/src/main/java/com/amplifyframework/datastore/storage/sqlite/SQLiteModelFieldTypeConverter.java b/aws-datastore/src/main/java/com/amplifyframework/datastore/storage/sqlite/SQLiteModelFieldTypeConverter.java index 71bf0770b2..42b70a839c 100644 --- a/aws-datastore/src/main/java/com/amplifyframework/datastore/storage/sqlite/SQLiteModelFieldTypeConverter.java +++ b/aws-datastore/src/main/java/com/amplifyframework/datastore/storage/sqlite/SQLiteModelFieldTypeConverter.java @@ -25,6 +25,7 @@ import com.amplifyframework.core.model.ModelField; import com.amplifyframework.core.model.ModelSchema; import com.amplifyframework.core.model.SchemaRegistry; +import com.amplifyframework.core.model.SerializedCustomType; import com.amplifyframework.core.model.SerializedModel; import com.amplifyframework.core.model.temporal.Temporal; import com.amplifyframework.core.model.types.JavaFieldType; @@ -309,6 +310,14 @@ public Object convertValueFromTarget(Model model, ModelField field) throws DataS Object fieldValue; if (model.getClass() == SerializedModel.class) { fieldValue = ((SerializedModel) model).getValue(field); + + // If the custom type has nested custom type its serializedData contains + // serialized custom type schema as well. + // We don't want to store entire SerializedCustomType along with schema into + // Database but only its value. + if (field.isCustomType() && fieldValue != null) { + fieldValue = ((SerializedCustomType) fieldValue).getFlatSerializedData(); + } } else { fieldValue = ModelHelper.getValue(model, field); } diff --git a/core/src/main/java/com/amplifyframework/core/model/SerializedCustomType.java b/core/src/main/java/com/amplifyframework/core/model/SerializedCustomType.java index 503023aef7..7c4cb19915 100644 --- a/core/src/main/java/com/amplifyframework/core/model/SerializedCustomType.java +++ b/core/src/main/java/com/amplifyframework/core/model/SerializedCustomType.java @@ -33,6 +33,7 @@ */ public final class SerializedCustomType { private final Map serializedData; + private Map flatSerializedData; private final CustomTypeSchema customTypeSchema; private SerializedCustomType(Map serializedData, CustomTypeSchema customTypeSchema) { @@ -126,6 +127,48 @@ public Map getSerializedData() { return serializedData; } + /** + * Gets the serialized data that doesn't contain SerializedCustomType structure. + * + * @return Serialized data + */ + @SuppressWarnings("unchecked") + @NonNull + public Map getFlatSerializedData() { + if (flatSerializedData != null) { + return flatSerializedData; + } + + flatSerializedData = new HashMap<>(); + + for (Map.Entry entry : serializedData.entrySet()) { + CustomTypeField field = customTypeSchema.getFields().get(entry.getKey()); + + if (field == null) { + continue; + } + + Object fieldValue = entry.getValue(); + + if (field.isCustomType() && fieldValue != null) { + if (field.isArray()) { + ArrayList items = (ArrayList) fieldValue; + ArrayList> flattenItems = new ArrayList<>(); + for (SerializedCustomType item : items) { + flattenItems.add(item.getFlatSerializedData()); + } + flatSerializedData.put(entry.getKey(), flattenItems); + } else { + flatSerializedData.put(entry.getKey(), ((SerializedCustomType) fieldValue).getFlatSerializedData()); + } + } else { + flatSerializedData.put(entry.getKey(), fieldValue); + } + } + + return flatSerializedData; + } + /** * Gets the CustomType schema. * diff --git a/core/src/test/java/com/amplifyframework/core/model/CustomTypeSchemaTest.java b/core/src/test/java/com/amplifyframework/core/model/CustomTypeSchemaTest.java index 9c1c5ecff7..ea0e677ad7 100644 --- a/core/src/test/java/com/amplifyframework/core/model/CustomTypeSchemaTest.java +++ b/core/src/test/java/com/amplifyframework/core/model/CustomTypeSchemaTest.java @@ -15,9 +15,17 @@ package com.amplifyframework.core.model; +import com.amplifyframework.testutils.Resources; + +import com.google.gson.Gson; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Before; import org.junit.Test; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import static org.junit.Assert.assertEquals; @@ -26,6 +34,128 @@ * Tests the {@link CustomTypeSchema}. */ public final class CustomTypeSchemaTest { + private CustomTypeSchema addressSchema; + private CustomTypeSchema contactSchema; + private CustomTypeSchema phoneSchema; + private CustomTypeSchema personInfoSchema; + + /** + * Create custom type schema for testing. + */ + @Before + public void setup() { + Map phoneFields = new HashMap<>(); + phoneFields.put( + "areaCode", CustomTypeField.builder() + .name("areaCode") + .javaClassForValue(String.class) + .targetType("String") + .build()); + phoneFields.put( + "number", CustomTypeField.builder() + .name("number") + .javaClassForValue(String.class) + .targetType("String") + .build()); + phoneFields.put( + "countryCode", CustomTypeField.builder() + .name("countryCode") + .javaClassForValue(String.class) + .targetType("String") + .build()); + phoneSchema = CustomTypeSchema.builder() + .name("Phone") + .pluralName("Phones") + .fields(phoneFields) + .build(); + + Map contactFields = new HashMap<>(); + contactFields.put( + "phone", CustomTypeField.builder() + .name("phone") + .javaClassForValue(Map.class) + .targetType("Phone") + .isCustomType(true) + .build()); + contactFields.put( + "email", CustomTypeField.builder() + .name("email") + .javaClassForValue(String.class) + .targetType("String") + .build()); + contactSchema = CustomTypeSchema.builder() + .name("Contact") + .pluralName("Contacts") + .fields(contactFields) + .build(); + + Map addressFields = new HashMap<>(); + addressFields.put( + "postalCode", CustomTypeField.builder() + .name("postalCode") + .javaClassForValue(String.class) + .targetType("String") + .build()); + addressFields.put( + "line1", CustomTypeField.builder() + .name("line1") + .javaClassForValue(String.class) + .targetType("String") + .build()); + addressFields.put( + "line2", CustomTypeField.builder() + .name("line2") + .javaClassForValue(String.class) + .targetType("String") + .build()); + addressFields.put( + "state", CustomTypeField.builder() + .name("state") + .javaClassForValue(String.class) + .targetType("String") + .build()); + addressSchema = CustomTypeSchema.builder() + .name("Address") + .pluralName("Addresses") + .fields(addressFields) + .build(); + + Map personInfoFields = new HashMap<>(); + personInfoFields.put( + "name", CustomTypeField.builder() + .name("name") + .javaClassForValue(String.class) + .targetType("String") + .build()); + personInfoFields.put( + "mailingAddresses", CustomTypeField.builder() + .name("mailingAddresses") + .javaClassForValue(List.class) + .targetType("Address") + .isCustomType(true) + .isArray(true) + .build()); + personInfoFields.put( + "contact", CustomTypeField.builder() + .name("contact") + .javaClassForValue(Map.class) + .targetType("Contact") + .isCustomType(true) + .build()); + personInfoFields.put( + "tags", CustomTypeField.builder() + .name("tags") + .isArray(true) + .javaClassForValue(List.class) + .targetType("String") + .build()); + personInfoSchema = CustomTypeSchema.builder() + .name("Person") + .pluralName("People") + .fields(personInfoFields) + .build(); + } + /** * The {@link CustomTypeSchema#builder()} should create expected CustomType schema. */ @@ -82,4 +212,77 @@ public void customTypeSchemaIsCreated() { assertEquals(typePluralName, schema.getPluralName()); assertEquals(fields, schema.getFields()); } + + /** + * The {@link SerializedCustomType#getFlatSerializedData()} should create a map of values. + * + * @throws JSONException On illegal json found by JSONAssert. + */ + @Test + public void customTypeGetFlatSerializedData() throws JSONException { + Map addressSerializedData1 = new HashMap<>(); + addressSerializedData1.put("line1", "222 Somewhere far"); + addressSerializedData1.put("line2", null); + addressSerializedData1.put("state", "CA"); + addressSerializedData1.put("postalCode", "123456"); + + SerializedCustomType address1 = SerializedCustomType.builder() + .serializedData(addressSerializedData1) + .customTypeSchema(addressSchema) + .build(); + + Map addressSerializedData2 = new HashMap<>(); + addressSerializedData2.put("line1", "444 Somewhere close"); + addressSerializedData2.put("line2", "Apt 3"); + addressSerializedData2.put("state", "WA"); + addressSerializedData2.put("postalCode", "123456"); + + SerializedCustomType address2 = SerializedCustomType.builder() + .serializedData(addressSerializedData2) + .customTypeSchema(addressSchema) + .build(); + + ArrayList addresses = new ArrayList<>(); + addresses.add(address1); + addresses.add(address2); + + Map phoneSerializedData = new HashMap<>(); + phoneSerializedData.put("countryCode", "1"); + phoneSerializedData.put("areaCode", "415"); + phoneSerializedData.put("phone", "6666666"); + + SerializedCustomType phone = SerializedCustomType.builder() + .serializedData(phoneSerializedData) + .customTypeSchema(phoneSchema) + .build(); + + Map contactSerializedData = new HashMap<>(); + contactSerializedData.put("email", "tester@testing.com"); + contactSerializedData.put("phone", phone); + + SerializedCustomType contact = SerializedCustomType.builder() + .serializedData(contactSerializedData) + .customTypeSchema(contactSchema) + .build(); + + ArrayList tags = new ArrayList<>(); + tags.add("string1"); + tags.add("string2"); + + Map personalSerializedData = new HashMap<>(); + personalSerializedData.put("name", "Tester Testing"); + personalSerializedData.put("mailingAddresses", addresses); + personalSerializedData.put("contact", contact); + personalSerializedData.put("tags", tags); + + SerializedCustomType person = SerializedCustomType.builder() + .serializedData(personalSerializedData) + .customTypeSchema(personInfoSchema) + .build(); + + String expectedResourcePath = "serialized-custom-type-nested-data.json"; + String expectedJson = Resources.readAsJson(expectedResourcePath).toString(2); + String actualJson = new JSONObject(new Gson().toJson(person.getFlatSerializedData())).toString(2); + assertEquals(expectedJson, actualJson); + } } diff --git a/core/src/test/resources/serialized-custom-type-nested-data.json b/core/src/test/resources/serialized-custom-type-nested-data.json new file mode 100644 index 0000000000..133cf412f1 --- /dev/null +++ b/core/src/test/resources/serialized-custom-type-nested-data.json @@ -0,0 +1,27 @@ +{ + "contact": { + "phone": { + "areaCode": "415", + "countryCode": "1" + }, + "email": "tester@testing.com" + }, + "name": "Tester Testing", + "mailingAddresses": [ + { + "postalCode": "123456", + "state": "CA", + "line1": "222 Somewhere far" + }, + { + "postalCode": "123456", + "state": "WA", + "line2": "Apt 3", + "line1": "444 Somewhere close" + } + ], + "tags": [ + "string1", + "string2" + ] +} \ No newline at end of file