diff --git a/sdk/core/azure-core-serializer-json-jackson/src/main/java/com/azure/core/serializer/json/jackson/implementation/JacksonDatabind212.java b/sdk/core/azure-core-serializer-json-jackson/src/main/java/com/azure/core/serializer/json/jackson/implementation/JacksonDatabind212.java
index bfcaee158f2aa..c9f84888898e4 100644
--- a/sdk/core/azure-core-serializer-json-jackson/src/main/java/com/azure/core/serializer/json/jackson/implementation/JacksonDatabind212.java
+++ b/sdk/core/azure-core-serializer-json-jackson/src/main/java/com/azure/core/serializer/json/jackson/implementation/JacksonDatabind212.java
@@ -4,13 +4,22 @@
package com.azure.core.serializer.json.jackson.implementation;
import com.azure.core.util.logging.ClientLogger;
+import com.fasterxml.jackson.databind.BeanDescription;
+import com.fasterxml.jackson.databind.DeserializationConfig;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.cfg.CoercionAction;
import com.fasterxml.jackson.databind.cfg.CoercionInputShape;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
+import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
+import com.fasterxml.jackson.databind.deser.std.DelegatingDeserializer;
import com.fasterxml.jackson.databind.introspect.AccessorNamingStrategy;
import com.fasterxml.jackson.databind.introspect.AnnotatedClass;
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.databind.util.AccessPattern;
/**
* Utility methods for Jackson Databind types when it's known that the version is 2.12+.
@@ -27,6 +36,36 @@ final class JacksonDatabind212 {
* @return The updated {@link ObjectMapper}.
*/
static ObjectMapper mutateXmlCoercions(ObjectMapper mapper) {
+ // https://github.com/FasterXML/jackson-dataformat-xml/pull/585/files fixed array and collection elements
+ // with coercion to be handled by the coercion config below which is a backwards compatibility breaking
+ // change for us. Handle empty string items within an array or collection as empty string.
+ mapper.registerModule(new SimpleModule().setDeserializerModifier(new BeanDeserializerModifier() {
+ @Override
+ public JsonDeserializer> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc,
+ JsonDeserializer> deserializer) {
+ if (String.class.isAssignableFrom(beanDesc.getBeanClass())) {
+ return new DelegatingDeserializer(deserializer) {
+ @Override
+ protected JsonDeserializer> newDelegatingInstance(JsonDeserializer> newDelegatee) {
+ return this;
+ }
+
+ @Override
+ public AccessPattern getNullAccessPattern() {
+ return AccessPattern.DYNAMIC;
+ }
+
+ @Override
+ public Object getNullValue(DeserializationContext ctxt) throws JsonMappingException {
+ return (ctxt.getParser().getParsingContext().inArray()) ? "" : super.getNullValue(ctxt);
+ }
+ };
+ } else {
+ return deserializer;
+ }
+ }
+ }));
+
mapper.coercionConfigDefaults().setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull);
return mapper;
}
diff --git a/sdk/core/azure-core-version-tests/pom.xml b/sdk/core/azure-core-version-tests/pom.xml
index 14a846605db5b..fe5ea1bc7951c 100644
--- a/sdk/core/azure-core-version-tests/pom.xml
+++ b/sdk/core/azure-core-version-tests/pom.xml
@@ -101,6 +101,11 @@
provided
+
+ com.azure
+ azure-core-test
+ 1.19.0-beta.1
+
io.projectreactor
reactor-test
diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/AdditionalPropertiesSerializerTests.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/AdditionalPropertiesSerializerTests.java
new file mode 100644
index 0000000000000..a7c36c00dd3c1
--- /dev/null
+++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/AdditionalPropertiesSerializerTests.java
@@ -0,0 +1,122 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.version.tests;
+
+import com.azure.core.util.serializer.JacksonAdapter;
+import com.azure.core.util.serializer.SerializerEncoding;
+import com.azure.core.version.tests.models.Foo;
+import com.azure.core.version.tests.models.FooChild;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public class AdditionalPropertiesSerializerTests {
+ @Test
+ public void canSerializeAdditionalProperties() throws Exception {
+ Foo foo = new Foo();
+ foo.bar("hello.world");
+ foo.baz(new ArrayList<>());
+ foo.baz().add("hello");
+ foo.baz().add("hello.world");
+ foo.qux(new HashMap<>());
+ foo.qux().put("hello", "world");
+ foo.qux().put("a.b", "c.d");
+ foo.qux().put("bar.a", "ttyy");
+ foo.qux().put("bar.b", "uuzz");
+ foo.additionalProperties(new HashMap<>());
+ foo.additionalProperties().put("bar", "baz");
+ foo.additionalProperties().put("a.b", "c.d");
+ foo.additionalProperties().put("properties.bar", "barbar");
+
+ String serialized = new JacksonAdapter().serialize(foo, SerializerEncoding.JSON);
+ Assertions.assertEquals("{\"$type\":\"foo\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}},\"bar\":\"baz\",\"a.b\":\"c.d\",\"properties.bar\":\"barbar\"}", serialized);
+ }
+
+ @Test
+ public void canDeserializeAdditionalProperties() throws Exception {
+ String wireValue = "{\"$type\":\"foo\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}},\"bar\":\"baz\",\"a.b\":\"c.d\",\"properties.bar\":\"barbar\"}";
+ Foo deserialized = new JacksonAdapter().deserialize(wireValue, Foo.class, SerializerEncoding.JSON);
+ Assertions.assertNotNull(deserialized.additionalProperties());
+ Assertions.assertEquals("baz", deserialized.additionalProperties().get("bar"));
+ Assertions.assertEquals("c.d", deserialized.additionalProperties().get("a.b"));
+ Assertions.assertEquals("barbar", deserialized.additionalProperties().get("properties.bar"));
+ }
+
+ @Test
+ public void canSerializeAdditionalPropertiesThroughInheritance() throws Exception {
+ Foo foo = new FooChild();
+ foo.bar("hello.world");
+ foo.baz(new ArrayList<>());
+ foo.baz().add("hello");
+ foo.baz().add("hello.world");
+ foo.qux(new HashMap<>());
+ foo.qux().put("hello", "world");
+ foo.qux().put("a.b", "c.d");
+ foo.qux().put("bar.a", "ttyy");
+ foo.qux().put("bar.b", "uuzz");
+ foo.additionalProperties(new HashMap<>());
+ foo.additionalProperties().put("bar", "baz");
+ foo.additionalProperties().put("a.b", "c.d");
+ foo.additionalProperties().put("properties.bar", "barbar");
+
+ String serialized = new JacksonAdapter().serialize(foo, SerializerEncoding.JSON);
+ Assertions.assertEquals("{\"$type\":\"foochild\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}},\"bar\":\"baz\",\"a.b\":\"c.d\",\"properties.bar\":\"barbar\"}", serialized);
+ }
+
+ @Test
+ public void canDeserializeAdditionalPropertiesThroughInheritance() throws Exception {
+ String wireValue = "{\"$type\":\"foochild\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}},\"bar\":\"baz\",\"a.b\":\"c.d\",\"properties.bar\":\"barbar\"}";
+ Foo deserialized = new JacksonAdapter().deserialize(wireValue, Foo.class, SerializerEncoding.JSON);
+ // Check additional properties are populated
+ Assertions.assertNotNull(deserialized.additionalProperties());
+ Assertions.assertEquals("baz", deserialized.additionalProperties().get("bar"));
+ Assertions.assertEquals("c.d", deserialized.additionalProperties().get("a.b"));
+ Assertions.assertEquals("barbar", deserialized.additionalProperties().get("properties.bar"));
+ Assertions.assertTrue(deserialized instanceof FooChild);
+ // Check typed properties are populated
+ Assertions.assertEquals("hello.world", deserialized.bar());
+ Assertions.assertNotNull(deserialized.baz());
+ Assertions.assertEquals(2, deserialized.baz().size());
+ Assertions.assertTrue(deserialized.baz().contains("hello"));
+ Assertions.assertTrue(deserialized.baz().contains("hello.world"));
+ Assertions.assertNotNull(deserialized.qux());
+ Assertions.assertEquals(4, deserialized.qux().size());
+ Assertions.assertTrue(deserialized.qux().containsKey("hello"));
+ Assertions.assertTrue(deserialized.qux().containsKey("a.b"));
+ Assertions.assertTrue(deserialized.qux().containsKey("bar.a"));
+ Assertions.assertTrue(deserialized.qux().containsKey("bar.b"));
+ Assertions.assertEquals("world", deserialized.qux().get("hello"));
+ Assertions.assertEquals("c.d", deserialized.qux().get("a.b"));
+ Assertions.assertEquals("ttyy", deserialized.qux().get("bar.a"));
+ Assertions.assertEquals("uuzz", deserialized.qux().get("bar.b"));
+ }
+
+ @Test
+ public void canSerializeAdditionalPropertiesWithNestedAdditionalProperties() throws Exception {
+ Foo foo = new Foo();
+ foo.bar("hello.world");
+ foo.baz(new ArrayList<>());
+ foo.baz().add("hello");
+ foo.baz().add("hello.world");
+ foo.qux(new HashMap<>());
+ foo.qux().put("hello", "world");
+ foo.qux().put("a.b", "c.d");
+ foo.qux().put("bar.a", "ttyy");
+ foo.qux().put("bar.b", "uuzz");
+ foo.additionalProperties(new HashMap<>());
+ foo.additionalProperties().put("bar", "baz");
+ foo.additionalProperties().put("a.b", "c.d");
+ foo.additionalProperties().put("properties.bar", "barbar");
+ Foo nestedFoo = new Foo();
+ nestedFoo.bar("bye.world");
+ nestedFoo.additionalProperties(new HashMap<>());
+ nestedFoo.additionalProperties().put("name", "Sushi");
+ foo.additionalProperties().put("foo", nestedFoo);
+
+ String serialized = new JacksonAdapter().serialize(foo, SerializerEncoding.JSON);
+ Assertions.assertEquals("{\"$type\":\"foo\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}},\"bar\":\"baz\",\"foo\":{\"properties\":{\"bar\":\"bye.world\"},\"name\":\"Sushi\"},\"a.b\":\"c.d\",\"properties.bar\":\"barbar\"}", serialized);
+ }
+}
diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/AdditionalPropertiesSerializerWithJacksonAnnotationTests.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/AdditionalPropertiesSerializerWithJacksonAnnotationTests.java
new file mode 100644
index 0000000000000..2383d7836a2c9
--- /dev/null
+++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/AdditionalPropertiesSerializerWithJacksonAnnotationTests.java
@@ -0,0 +1,140 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.version.tests;
+
+import com.azure.core.util.serializer.JacksonAdapter;
+import com.azure.core.util.serializer.SerializerEncoding;
+import com.azure.core.version.tests.models.NewFoo;
+import com.azure.core.version.tests.models.NewFooChild;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public class AdditionalPropertiesSerializerWithJacksonAnnotationTests {
+ @Test
+ public void canSerializeAdditionalProperties() throws Exception {
+ NewFoo foo = new NewFoo();
+ foo.bar("hello.world");
+ foo.baz(new ArrayList<>());
+ foo.baz().add("hello");
+ foo.baz().add("hello.world");
+ foo.qux(new HashMap<>());
+ foo.qux().put("hello", "world");
+ foo.qux().put("a.b", "c.d");
+ foo.qux().put("bar.a", "ttyy");
+ foo.qux().put("bar.b", "uuzz");
+ foo.additionalProperties(new HashMap<>());
+ foo.additionalProperties().put("bar", "baz");
+ foo.additionalProperties().put("a.b", "c.d");
+ foo.additionalProperties().put("properties.bar", "barbar");
+
+ String serialized = new JacksonAdapter().serialize(foo, SerializerEncoding.JSON);
+ Assertions.assertEquals("{\"$type\":\"newfoo\",\"bar\":\"baz\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}},\"a.b\":\"c.d\",\"properties.bar\":\"barbar\"}", serialized);
+ }
+
+ @Test
+ public void canDeserializeAdditionalProperties() throws Exception {
+ String wireValue = "{\"$type\":\"newfoo\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}},\"bar\":\"baz\",\"a.b\":\"c.d\",\"properties.bar\":\"barbar\"}";
+ NewFoo deserialized = new JacksonAdapter().deserialize(wireValue, NewFoo.class, SerializerEncoding.JSON);
+ Assertions.assertNotNull(deserialized.additionalProperties());
+ Assertions.assertEquals("baz", deserialized.additionalProperties().get("bar"));
+ Assertions.assertEquals("c.d", deserialized.additionalProperties().get("a.b"));
+ Assertions.assertEquals("barbar", deserialized.additionalProperties().get("properties.bar"));
+ }
+
+ @Test
+ public void canSerializeAdditionalPropertiesThroughInheritance() throws Exception {
+ NewFoo foo = new NewFooChild();
+ foo.bar("hello.world");
+ foo.baz(new ArrayList<>());
+ foo.baz().add("hello");
+ foo.baz().add("hello.world");
+ foo.qux(new HashMap<>());
+ foo.qux().put("hello", "world");
+ foo.qux().put("a.b", "c.d");
+ foo.qux().put("bar.a", "ttyy");
+ foo.qux().put("bar.b", "uuzz");
+ foo.additionalProperties(new HashMap<>());
+ foo.additionalProperties().put("bar", "baz");
+ foo.additionalProperties().put("a.b", "c.d");
+ foo.additionalProperties().put("properties.bar", "barbar");
+
+ String serialized = new JacksonAdapter().serialize(foo, SerializerEncoding.JSON);
+ Assertions.assertEquals("{\"$type\":\"newfoochild\",\"bar\":\"baz\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}},\"a.b\":\"c.d\",\"properties.bar\":\"barbar\"}", serialized);
+ }
+
+ @Test
+ public void canDeserializeAdditionalPropertiesThroughInheritance() throws Exception {
+ String wireValue = "{\"$type\":\"newfoochild\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}},\"bar\":\"baz\",\"a.b\":\"c.d\",\"properties.bar\":\"barbar\"}";
+ NewFoo deserialized = new JacksonAdapter().deserialize(wireValue, NewFoo.class, SerializerEncoding.JSON);
+ Assertions.assertNotNull(deserialized.additionalProperties());
+ Assertions.assertEquals("baz", deserialized.additionalProperties().get("bar"));
+ Assertions.assertEquals("c.d", deserialized.additionalProperties().get("a.b"));
+ Assertions.assertEquals("barbar", deserialized.additionalProperties().get("properties.bar"));
+ Assertions.assertTrue(deserialized instanceof NewFooChild);
+ }
+
+ @Test
+ public void canSerializeAdditionalPropertiesWithNestedAdditionalProperties() throws Exception {
+ NewFoo foo = new NewFoo();
+ foo.bar("hello.world");
+ foo.baz(new ArrayList<>());
+ foo.baz().add("hello");
+ foo.baz().add("hello.world");
+ foo.qux(new HashMap<>());
+ foo.qux().put("hello", "world");
+ foo.qux().put("a.b", "c.d");
+ foo.qux().put("bar.a", "ttyy");
+ foo.qux().put("bar.b", "uuzz");
+ foo.additionalProperties(new HashMap<>());
+ foo.additionalProperties().put("bar", "baz");
+ foo.additionalProperties().put("a.b", "c.d");
+ foo.additionalProperties().put("properties.bar", "barbar");
+ NewFoo nestedNewFoo = new NewFoo();
+ nestedNewFoo.bar("bye.world");
+ nestedNewFoo.additionalProperties(new HashMap<>());
+ nestedNewFoo.additionalProperties().put("name", "Sushi");
+ foo.additionalProperties().put("foo", nestedNewFoo);
+
+ String serialized = new JacksonAdapter().serialize(foo, SerializerEncoding.JSON);
+ Assertions.assertEquals("{\"$type\":\"newfoo\",\"bar\":\"baz\",\"foo\":{\"name\":\"Sushi\",\"properties\":{\"bar\":\"bye.world\"}},\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}},\"a.b\":\"c.d\",\"properties.bar\":\"barbar\"}", serialized);
+ }
+
+ @Test
+ public void canSerializeAdditionalPropertiesWithConflictProperty() throws Exception {
+ NewFoo foo = new NewFoo();
+ foo.bar("hello.world");
+ foo.baz(new ArrayList<>());
+ foo.baz().add("hello");
+ foo.baz().add("hello.world");
+ foo.qux(new HashMap<>());
+ foo.qux().put("hello", "world");
+ foo.qux().put("a.b", "c.d");
+ foo.qux().put("bar.a", "ttyy");
+ foo.qux().put("bar.b", "uuzz");
+ foo.additionalProperties(new HashMap<>());
+ foo.additionalProperties().put("bar", "baz");
+ foo.additionalProperties().put("a.b", "c.d");
+ foo.additionalProperties().put("properties.bar", "barbar");
+ foo.additionalPropertiesProperty(new HashMap<>());
+ foo.additionalPropertiesProperty().put("age", 73);
+
+ String serialized = new JacksonAdapter().serialize(foo, SerializerEncoding.JSON);
+ Assertions.assertEquals("{\"$type\":\"newfoo\",\"additionalProperties\":{\"age\":73},\"bar\":\"baz\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}},\"a.b\":\"c.d\",\"properties.bar\":\"barbar\"}", serialized);
+ }
+
+ @Test
+ public void canDeserializeAdditionalPropertiesWithConflictProperty() throws Exception {
+ String wireValue = "{\"$type\":\"newfoo\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}},\"bar\":\"baz\",\"a.b\":\"c.d\",\"properties.bar\":\"barbar\",\"additionalProperties\":{\"age\":73}}";
+ NewFoo deserialized = new JacksonAdapter().deserialize(wireValue, NewFoo.class, SerializerEncoding.JSON);
+ Assertions.assertNotNull(deserialized.additionalProperties());
+ Assertions.assertEquals("baz", deserialized.additionalProperties().get("bar"));
+ Assertions.assertEquals("c.d", deserialized.additionalProperties().get("a.b"));
+ Assertions.assertEquals("barbar", deserialized.additionalProperties().get("properties.bar"));
+ Assertions.assertEquals(1, deserialized.additionalPropertiesProperty().size());
+ Assertions.assertEquals(73, deserialized.additionalPropertiesProperty().get("age"));
+ }
+}
diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/BinaryDataSerializationTests.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/BinaryDataSerializationTests.java
new file mode 100644
index 0000000000000..9152bd81b89fc
--- /dev/null
+++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/BinaryDataSerializationTests.java
@@ -0,0 +1,373 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.version.tests;
+
+import com.azure.core.util.BinaryData;
+import com.azure.core.util.serializer.JacksonAdapter;
+import com.azure.core.util.serializer.SerializerAdapter;
+import com.azure.core.util.serializer.SerializerEncoding;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.junit.jupiter.api.parallel.Execution;
+import org.junit.jupiter.api.parallel.ExecutionMode;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+
+import static java.util.Collections.singletonList;
+import static java.util.Collections.singletonMap;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class BinaryDataSerializationTests {
+ private static final SerializerAdapter ADAPTER = JacksonAdapter.createDefaultSerializerAdapter();
+
+ private static final byte[] HELLO_BYTES = "hello".getBytes(StandardCharsets.UTF_8);
+ private static final String BASE64_HELLO_BYTES = Base64.getEncoder().encodeToString(HELLO_BYTES);
+ private static final SimplePojo SIMPLE_POJO = new SimplePojo()
+ .setString("hello")
+ .setNumber(3.14)
+ .setBool(true);
+
+ private static final BinaryData FROM_BYTES = BinaryData.fromBytes(HELLO_BYTES);
+ private static final Supplier FROM_STREAM
+ = () -> BinaryData.fromStream(new ByteArrayInputStream(HELLO_BYTES), (long) HELLO_BYTES.length);
+ private static final BinaryData FROM_STRING = BinaryData.fromString("hello");
+ private static final BinaryData FROM_OBJECT = BinaryData.fromObject(SIMPLE_POJO);
+ private static final BinaryData FROM_STRING_BASE64 = BinaryData.fromString(BASE64_HELLO_BYTES);
+
+ private static final String EMPTY_OBJECT_JSON = "{}";
+
+ private static final String SIMPLE_PROPERTY_BYTES_JSON = "{\"simpleProperty\":\"" + BASE64_HELLO_BYTES + "\"}";
+ private static final String SIMPLE_PROPERTY_STREAM_JSON = "{\"simpleProperty\":\"" + BASE64_HELLO_BYTES + "\"}";
+ private static final String SIMPLE_PROPERTY_STRING_JSON = "{\"simpleProperty\":\"hello\"}";
+ private static final String SIMPLE_PROPERTY_OBJECT_JSON
+ = "{\"simpleProperty\":{\"string\":\"hello\",\"number\":3.14,\"boolean\":true}}";
+
+ private static final SimpleProperty SIMPLE_PROPERTY_NULL = new SimpleProperty();
+ private static final SimpleProperty SIMPLE_PROPERTY_FROM_BYTES = new SimpleProperty()
+ .setSimpleProperty(FROM_BYTES);
+ private static final SimpleProperty SIMPLE_PROPERTY_FROM_STREAM = new SimpleProperty()
+ .setSimpleProperty(FROM_STREAM.get());
+ private static final SimpleProperty SIMPLE_PROPERTY_FROM_STRING = new SimpleProperty()
+ .setSimpleProperty(FROM_STRING);
+ private static final SimpleProperty SIMPLE_PROPERTY_FROM_OBJECT = new SimpleProperty()
+ .setSimpleProperty(FROM_OBJECT);
+ private static final SimpleProperty SIMPLE_PROPERTY_BASE64 = new SimpleProperty()
+ .setSimpleProperty(FROM_STRING_BASE64);
+
+ private static final String LIST_PROPERTY_BYTES_JSON = "{\"listProperty\":[\"" + BASE64_HELLO_BYTES + "\"]}";
+ private static final String LIST_PROPERTY_STREAM_JSON = "{\"listProperty\":[\"" + BASE64_HELLO_BYTES + "\"]}";
+ private static final String LIST_PROPERTY_STRING_JSON = "{\"listProperty\":[\"hello\"]}";
+ private static final String LIST_PROPERTY_OBJECT_JSON
+ = "{\"listProperty\":[{\"string\":\"hello\",\"number\":3.14,\"boolean\":true}]}";
+ private static final String LIST_PROPERTY_MULTI_JSON
+ = "{\"listProperty\":[\"" + BASE64_HELLO_BYTES + "\",\"" + BASE64_HELLO_BYTES + "\",\"hello\","
+ + "{\"string\":\"hello\",\"number\":3.14,\"boolean\":true}]}";
+
+ private static final ListProperty LIST_PROPERTY_NULL = new ListProperty();
+ private static final ListProperty LIST_PROPERTY_FROM_BYTES = new ListProperty()
+ .setListProperty(singletonList(FROM_BYTES));
+ private static final ListProperty LIST_PROPERTY_FROM_STREAM = new ListProperty()
+ .setListProperty(singletonList(FROM_STREAM.get()));
+ private static final ListProperty LIST_PROPERTY_FROM_STRING = new ListProperty()
+ .setListProperty(singletonList(FROM_STRING));
+ private static final ListProperty LIST_PROPERTY_FROM_OBJECT = new ListProperty()
+ .setListProperty(singletonList(FROM_OBJECT));
+ private static final ListProperty LIST_PROPERTY_MULTIPLE = new ListProperty()
+ .setListProperty(Arrays.asList(FROM_BYTES, FROM_STREAM.get(), FROM_STRING, FROM_OBJECT));
+ private static final ListProperty LIST_PROPERTY_BASE64 = new ListProperty()
+ .setListProperty(singletonList(FROM_STRING_BASE64));
+ private static final ListProperty LIST_PROPERTY_BASE64_MULTIPLE = new ListProperty()
+ .setListProperty(Arrays.asList(FROM_STRING_BASE64, FROM_STRING_BASE64, FROM_STRING, FROM_OBJECT));
+
+ private static final String MAP_PROPERTY_BYTES_JSON = "{\"mapProperty\":{\"key\":\"" + BASE64_HELLO_BYTES + "\"}}";
+ private static final String MAP_PROPERTY_STREAM_JSON = "{\"mapProperty\":{\"key\":\"" + BASE64_HELLO_BYTES + "\"}}";
+ private static final String MAP_PROPERTY_STRING_JSON = "{\"mapProperty\":{\"key\":\"hello\"}}";
+ private static final String MAP_PROPERTY_OBJECT_JSON
+ = "{\"mapProperty\":{\"key\":{\"string\":\"hello\",\"number\":3.14,\"boolean\":true}}}";
+ private static final String MAP_PROPERTY_MULTI_JSON = "{\"mapProperty\":{"
+ + "\"fromBytes\":\"" + BASE64_HELLO_BYTES + "\","
+ + "\"fromStream\":\"" + BASE64_HELLO_BYTES + "\","
+ + "\"fromString\":\"hello\","
+ + "\"fromObject\":{\"string\":\"hello\",\"number\":3.14,\"boolean\":true}}}";
+
+ private static final MapProperty MAP_PROPERTY_NULL = new MapProperty();
+ private static final MapProperty MAP_PROPERTY_FROM_BYTES = new MapProperty()
+ .setMapProperty(singletonMap("key", FROM_BYTES));
+ private static final MapProperty MAP_PROPERTY_FROM_STREAM = new MapProperty()
+ .setMapProperty(singletonMap("key", FROM_STREAM.get()));
+ private static final MapProperty MAP_PROPERTY_FROM_STRING = new MapProperty()
+ .setMapProperty(singletonMap("key", FROM_STRING));
+ private static final MapProperty MAP_PROPERTY_FROM_OBJECT = new MapProperty()
+ .setMapProperty(singletonMap("key", FROM_OBJECT));
+ private static final MapProperty MAP_PROPERTY_MULTIPLE = new MapProperty()
+ .setMapProperty(createMapPropertyMultiMap(false));
+ private static final MapProperty MAP_PROPERTY_BASE64 = new MapProperty()
+ .setMapProperty(singletonMap("key", FROM_STRING_BASE64));
+ private static final MapProperty MAP_PROPERTY_BASE64_MULTIPLE = new MapProperty()
+ .setMapProperty(createMapPropertyMultiMap(true));
+
+ private static Map createMapPropertyMultiMap(boolean base64) {
+ Map map = new LinkedHashMap<>();
+ map.put("fromBytes", base64 ? FROM_STRING_BASE64 : FROM_BYTES);
+ map.put("fromStream", base64 ? FROM_STRING_BASE64 : FROM_STREAM.get());
+ map.put("fromString", FROM_STRING);
+ map.put("fromObject", FROM_OBJECT);
+
+ return map;
+ }
+
+ @ParameterizedTest
+ @MethodSource("binaryDataSerializationSupplier")
+ public void binaryDataSerialization(Object serializable, String expected) throws IOException {
+ String actual = ADAPTER.serialize(serializable, SerializerEncoding.JSON);
+
+ assertEquals(expected, actual);
+ }
+
+ private static Stream binaryDataSerializationSupplier() {
+ return Stream.of(
+ Arguments.of(SIMPLE_PROPERTY_NULL, EMPTY_OBJECT_JSON),
+ Arguments.of(SIMPLE_PROPERTY_FROM_BYTES, SIMPLE_PROPERTY_BYTES_JSON),
+ Arguments.of(SIMPLE_PROPERTY_FROM_STREAM, SIMPLE_PROPERTY_STREAM_JSON),
+ Arguments.of(SIMPLE_PROPERTY_FROM_STRING, SIMPLE_PROPERTY_STRING_JSON),
+ Arguments.of(SIMPLE_PROPERTY_FROM_OBJECT, SIMPLE_PROPERTY_OBJECT_JSON),
+
+ Arguments.of(LIST_PROPERTY_NULL, EMPTY_OBJECT_JSON),
+ Arguments.of(LIST_PROPERTY_FROM_BYTES, LIST_PROPERTY_BYTES_JSON),
+ Arguments.of(LIST_PROPERTY_FROM_STREAM, LIST_PROPERTY_STREAM_JSON),
+ Arguments.of(LIST_PROPERTY_FROM_STRING, LIST_PROPERTY_STRING_JSON),
+ Arguments.of(LIST_PROPERTY_FROM_OBJECT, LIST_PROPERTY_OBJECT_JSON),
+ Arguments.of(LIST_PROPERTY_MULTIPLE, LIST_PROPERTY_MULTI_JSON),
+
+ Arguments.of(MAP_PROPERTY_NULL, EMPTY_OBJECT_JSON),
+ Arguments.of(MAP_PROPERTY_FROM_BYTES, MAP_PROPERTY_BYTES_JSON),
+ Arguments.of(MAP_PROPERTY_FROM_STREAM, MAP_PROPERTY_STREAM_JSON),
+ Arguments.of(MAP_PROPERTY_FROM_STRING, MAP_PROPERTY_STRING_JSON),
+ Arguments.of(MAP_PROPERTY_FROM_OBJECT, MAP_PROPERTY_OBJECT_JSON),
+ Arguments.of(MAP_PROPERTY_MULTIPLE, MAP_PROPERTY_MULTI_JSON)
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("binaryDataDeserializationSupplier")
+ @Execution(ExecutionMode.SAME_THREAD)
+ public void binaryDataDeserialization(Object expected, String json, Class> type) throws IOException {
+ Object actual = ADAPTER.deserialize(json, type, SerializerEncoding.JSON);
+
+ assertEquals(expected, actual);
+ }
+
+ private static Stream binaryDataDeserializationSupplier() {
+ return Stream.of(
+ Arguments.of(SIMPLE_PROPERTY_NULL, EMPTY_OBJECT_JSON, SimpleProperty.class),
+ Arguments.of(SIMPLE_PROPERTY_BASE64, SIMPLE_PROPERTY_BYTES_JSON, SimpleProperty.class),
+ Arguments.of(SIMPLE_PROPERTY_BASE64, SIMPLE_PROPERTY_STREAM_JSON, SimpleProperty.class),
+ Arguments.of(SIMPLE_PROPERTY_FROM_STRING, SIMPLE_PROPERTY_STRING_JSON, SimpleProperty.class),
+ Arguments.of(SIMPLE_PROPERTY_FROM_OBJECT, SIMPLE_PROPERTY_OBJECT_JSON, SimpleProperty.class),
+
+ Arguments.of(LIST_PROPERTY_NULL, EMPTY_OBJECT_JSON, ListProperty.class),
+ Arguments.of(LIST_PROPERTY_BASE64, LIST_PROPERTY_BYTES_JSON, ListProperty.class),
+ Arguments.of(LIST_PROPERTY_BASE64, LIST_PROPERTY_STREAM_JSON, ListProperty.class),
+ Arguments.of(LIST_PROPERTY_FROM_STRING, LIST_PROPERTY_STRING_JSON, ListProperty.class),
+ Arguments.of(LIST_PROPERTY_FROM_OBJECT, LIST_PROPERTY_OBJECT_JSON, ListProperty.class),
+ Arguments.of(LIST_PROPERTY_BASE64_MULTIPLE, LIST_PROPERTY_MULTI_JSON, ListProperty.class),
+
+ Arguments.of(MAP_PROPERTY_NULL, EMPTY_OBJECT_JSON, MapProperty.class),
+ Arguments.of(MAP_PROPERTY_BASE64, MAP_PROPERTY_BYTES_JSON, MapProperty.class),
+ Arguments.of(MAP_PROPERTY_BASE64, MAP_PROPERTY_STREAM_JSON, MapProperty.class),
+ Arguments.of(MAP_PROPERTY_FROM_STRING, MAP_PROPERTY_STRING_JSON, MapProperty.class),
+ Arguments.of(MAP_PROPERTY_FROM_OBJECT, MAP_PROPERTY_OBJECT_JSON, MapProperty.class),
+ Arguments.of(MAP_PROPERTY_BASE64_MULTIPLE, MAP_PROPERTY_MULTI_JSON, MapProperty.class)
+ );
+ }
+
+ public static final class SimpleProperty {
+ @JsonProperty("simpleProperty")
+ private BinaryData simpleProperty;
+
+ public BinaryData getSimpleProperty() {
+ return simpleProperty;
+ }
+
+ public SimpleProperty setSimpleProperty(BinaryData simpleProperty) {
+ this.simpleProperty = simpleProperty;
+ return this;
+ }
+
+ @Override
+ public int hashCode() {
+ return binaryDataHash(simpleProperty);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof SimpleProperty)) {
+ return false;
+ }
+
+ SimpleProperty other = (SimpleProperty) obj;
+ return binaryDataEquals(simpleProperty, other.simpleProperty);
+ }
+ }
+
+ public static final class ListProperty {
+ @JsonProperty("listProperty")
+ private List listProperty;
+
+ public List getListProperty() {
+ return listProperty;
+ }
+
+ public ListProperty setListProperty(List listProperty) {
+ this.listProperty = listProperty;
+ return this;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(listProperty);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof ListProperty)) {
+ return false;
+ }
+
+ ListProperty other = (ListProperty) obj;
+
+ if (listProperty == null && other.listProperty == null) {
+ return true;
+ } else if (listProperty != null && other.listProperty == null) {
+ return false;
+ } else if (listProperty == null && other.listProperty != null) {
+ return false;
+ }
+
+ if (listProperty.size() != other.listProperty.size()) {
+ return false;
+ }
+
+ int size = listProperty.size();
+ for (int i = 0; i < size; i++) {
+ if (!binaryDataEquals(listProperty.get(i), other.listProperty.get(i))) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+
+ public static final class MapProperty {
+ @JsonProperty("mapProperty")
+ private Map mapProperty;
+
+ public Map getMapProperty() {
+ return mapProperty;
+ }
+
+ public MapProperty setMapProperty(Map mapProperty) {
+ this.mapProperty = mapProperty;
+ return this;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mapProperty);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof MapProperty)) {
+ return false;
+ }
+
+ MapProperty other = (MapProperty) obj;
+
+ if (mapProperty == null && other.mapProperty == null) {
+ return true;
+ } else if (mapProperty != null && other.mapProperty == null) {
+ return false;
+ } else if (mapProperty == null && other.mapProperty != null) {
+ return false;
+ }
+
+ if (mapProperty.size() != other.mapProperty.size()) {
+ return false;
+ }
+
+ for (Map.Entry entry : mapProperty.entrySet()) {
+ if (!other.mapProperty.containsKey(entry.getKey())) {
+ return false;
+ }
+
+ if (!binaryDataEquals(entry.getValue(), other.mapProperty.get(entry.getKey()))) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+
+ public static final class SimplePojo {
+ @JsonProperty("string")
+ private String string;
+
+ @JsonProperty("number")
+ private double number;
+
+ @JsonProperty("boolean")
+ private boolean bool;
+
+ public String getString() {
+ return string;
+ }
+
+ public SimplePojo setString(String string) {
+ this.string = string;
+ return this;
+ }
+
+ public double getNumber() {
+ return number;
+ }
+
+ public SimplePojo setNumber(double number) {
+ this.number = number;
+ return this;
+ }
+
+ public boolean isBool() {
+ return bool;
+ }
+
+ public SimplePojo setBool(boolean bool) {
+ this.bool = bool;
+ return this;
+ }
+ }
+
+ private static int binaryDataHash(BinaryData data) {
+ return Objects.hash(data == null ? null : data.toString());
+ }
+
+ private static boolean binaryDataEquals(BinaryData data1, BinaryData data2) {
+ return (data1 == null && data2 == null)
+ || (data1 != null && data2 != null && Objects.equals(data1.toString(), data2.toString()));
+ }
+}
diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/CustomSerializerTests.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/CustomSerializerTests.java
new file mode 100644
index 0000000000000..3e04758afa25d
--- /dev/null
+++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/CustomSerializerTests.java
@@ -0,0 +1,376 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.version.tests;
+
+import com.azure.core.models.GeoBoundingBox;
+import com.azure.core.models.GeoCollection;
+import com.azure.core.models.GeoLineString;
+import com.azure.core.models.GeoLineStringCollection;
+import com.azure.core.models.GeoLinearRing;
+import com.azure.core.models.GeoObject;
+import com.azure.core.models.GeoObjectType;
+import com.azure.core.models.GeoPoint;
+import com.azure.core.models.GeoPointCollection;
+import com.azure.core.models.GeoPolygon;
+import com.azure.core.models.GeoPolygonCollection;
+import com.azure.core.models.GeoPosition;
+import com.azure.core.util.serializer.JacksonAdapter;
+import com.azure.core.util.serializer.SerializerAdapter;
+import com.azure.core.util.serializer.SerializerEncoding;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Tests for {@code DateTimeDeserializer}.
+ */
+public class CustomSerializerTests {
+ private static final SerializerAdapter ADAPTER = JacksonAdapter.createDefaultSerializerAdapter();
+
+ @ParameterizedTest
+ @MethodSource("offsetDateTimeDeserializationSupplier")
+ public void deserializeOffsetDateTime(String dateTimeJson, OffsetDateTime expected) throws IOException {
+ assertEquals(expected, ADAPTER.deserialize(dateTimeJson, OffsetDateTime.class, SerializerEncoding.JSON));
+ }
+
+ private static Stream offsetDateTimeDeserializationSupplier() {
+ return offsetDateTimeSupplier(true);
+ }
+
+ @ParameterizedTest
+ @MethodSource("offsetDateTimeSerializationSupplier")
+ public void serializeOffsetDateTime(String expected, OffsetDateTime dateTime) throws IOException {
+ assertEquals(expected, ADAPTER.serialize(dateTime, SerializerEncoding.JSON));
+ }
+
+ private static Stream offsetDateTimeSerializationSupplier() {
+ return offsetDateTimeSupplier(false);
+ }
+
+ private static Stream offsetDateTimeSupplier(boolean deserialization) {
+ OffsetDateTime minValue = OffsetDateTime.of(1, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC);
+ OffsetDateTime unixEpoch = OffsetDateTime.of(1970, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC);
+
+ OffsetDateTime nonUtcTimeZone = OffsetDateTime.of(2020, 1, 1, 0, 0, 0, 0, ZoneOffset.ofHours(-7));
+
+ if (deserialization) {
+ return Stream.of(
+ Arguments.of("\"0001-01-01T00:00:00\"", minValue),
+ Arguments.of(String.valueOf(minValue.toEpochSecond()), minValue),
+ Arguments.of("\"0001-01-01T00:00:00Z\"", minValue),
+ Arguments.of("\"1970-01-01T00:00:00\"", unixEpoch),
+ Arguments.of("\"1970-01-01T00:00:00Z\"", unixEpoch),
+ Arguments.of("\"2020-01-01T00:00:00-07:00\"", nonUtcTimeZone)
+ );
+ } else {
+ return Stream.of(
+ Arguments.of("\"0001-01-01T00:00:00Z\"", minValue),
+ Arguments.of("\"0001-01-01T00:00:00Z\"", minValue),
+ Arguments.of("\"1970-01-01T00:00:00Z\"", unixEpoch),
+ Arguments.of("\"1970-01-01T00:00:00Z\"", unixEpoch),
+ Arguments.of("\"2020-01-01T07:00:00Z\"", nonUtcTimeZone)
+ );
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("toStringTestSupplier")
+ public void toStringTest(Duration duration, String expected) throws IOException {
+ assertEquals(expected, ADAPTER.serialize(duration, SerializerEncoding.JSON));
+ }
+
+ private static Stream toStringTestSupplier() {
+ return Stream.of(
+ Arguments.of(Duration.ofMillis(0), "\"PT0S\""),
+ Arguments.of(Duration.ofMillis(1), "\"PT0.001S\""),
+ Arguments.of(Duration.ofMillis(9), "\"PT0.009S\""),
+ Arguments.of(Duration.ofMillis(10), "\"PT0.01S\""),
+ Arguments.of(Duration.ofMillis(11), "\"PT0.011S\""),
+ Arguments.of(Duration.ofMillis(99), "\"PT0.099S\""),
+ Arguments.of(Duration.ofMillis(100), "\"PT0.1S\""),
+ Arguments.of(Duration.ofMillis(101), "\"PT0.101S\""),
+ Arguments.of(Duration.ofMillis(999), "\"PT0.999S\""),
+ Arguments.of(Duration.ofMillis(1000), "\"PT1S\""),
+ Arguments.of(Duration.ofSeconds(1), "\"PT1S\""),
+ Arguments.of(Duration.ofSeconds(9), "\"PT9S\""),
+ Arguments.of(Duration.ofSeconds(10), "\"PT10S\""),
+ Arguments.of(Duration.ofSeconds(11), "\"PT11S\""),
+ Arguments.of(Duration.ofSeconds(59), "\"PT59S\""),
+ Arguments.of(Duration.ofSeconds(60), "\"PT1M\""),
+ Arguments.of(Duration.ofSeconds(61), "\"PT1M1S\""),
+ Arguments.of(Duration.ofMinutes(1), "\"PT1M\""),
+ Arguments.of(Duration.ofMinutes(9), "\"PT9M\""),
+ Arguments.of(Duration.ofMinutes(10), "\"PT10M\""),
+ Arguments.of(Duration.ofMinutes(11), "\"PT11M\""),
+ Arguments.of(Duration.ofMinutes(59), "\"PT59M\""),
+ Arguments.of(Duration.ofMinutes(60), "\"PT1H\""),
+ Arguments.of(Duration.ofMinutes(61), "\"PT1H1M\""),
+ Arguments.of(Duration.ofHours(1), "\"PT1H\""),
+ Arguments.of(Duration.ofHours(9), "\"PT9H\""),
+ Arguments.of(Duration.ofHours(10), "\"PT10H\""),
+ Arguments.of(Duration.ofHours(11), "\"PT11H\""),
+ Arguments.of(Duration.ofHours(23), "\"PT23H\""),
+ Arguments.of(Duration.ofHours(24), "\"P1D\""),
+ Arguments.of(Duration.ofHours(25), "\"P1DT1H\""),
+ Arguments.of(Duration.ofDays(1), "\"P1D\""),
+ Arguments.of(Duration.ofDays(9), "\"P9D\""),
+ Arguments.of(Duration.ofDays(10), "\"P10D\""),
+ Arguments.of(Duration.ofDays(11), "\"P11D\""),
+ Arguments.of(Duration.ofDays(99), "\"P99D\""),
+ Arguments.of(Duration.ofDays(100), "\"P100D\""),
+ Arguments.of(Duration.ofDays(101), "\"P101D\"")
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("invalidGeoJsonDeserializationSupplier")
+ public void invalidGeoJsonDeserializationThrowsIllegalStateException(String invalidGeoJson) {
+ Assertions.assertThrows(IllegalStateException.class,
+ () -> ADAPTER.deserialize(invalidGeoJson, GeoObject.class, SerializerEncoding.JSON));
+ }
+
+ private static Stream invalidGeoJsonDeserializationSupplier() {
+ return Stream.of(
+ "{\"coordinates\":[0,0]}", // Missing type
+ "{\"type\":\"Point\"}", // Missing coordinates
+ "{\"type\":\"Custom\",\"coordinates\":[0,0]}", // Invalid/unknown type
+ "{\"type\":\"Point\",\"coordinates\":[1]}", // Invalid coordinates count
+ "{\"type\":\"Point\",\"coordinates\":[4,4,4,4]}", // Invalid coordinates count
+ "{\"type\":\"Point\",\"coordinates\":[0,0],\"bbox\":[2,2]}", // Invalid bounding box
+ "{\"type\":\"Point\",\"coordinates\":[0,0],\"bbox\":[8,8,8,8,8,8,8,8]}", // Invalid bounding box
+ "{\"type\":\"GeometryCollection\"}" // Collection without geometries
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("geoJsonDeserializationSupplier")
+ public void geoJsonDeserialization(String json, Class type, T expectedGeo) throws IOException {
+ Assertions.assertEquals(expectedGeo, ADAPTER.deserialize(json, type, SerializerEncoding.JSON));
+ }
+
+ private static Stream geoJsonDeserializationSupplier() {
+ GeoBoundingBox boundingBox = new GeoBoundingBox(0, 0, 1, 1, 0D, 1D);
+ Map simpleProperties = Collections.singletonMap("key", "value");
+ Map arrayProperties = Collections.singletonMap("text", Arrays.asList("hello", "world"));
+
+ Map crs = new HashMap<>();
+ crs.put("type", "name");
+ crs.put("properties", Collections.singletonMap("name", "EPSG:432"));
+ Map objectProperties = Collections.singletonMap("crs", crs);
+
+ BiFunction, GeoPoint> pointSupplier =
+ (box, properties) -> new GeoPoint(new GeoPosition(0, 0, 0D), box, properties);
+
+ List positions = Arrays.asList(new GeoPosition(0, 0, 1D),
+ new GeoPosition(1, 1, 1D));
+ BiFunction, GeoLineString> lineSupplier =
+ (box, properties) -> new GeoLineString(positions, box, properties);
+
+ List rings = Collections.singletonList(new GeoLinearRing(Arrays.asList(
+ new GeoPosition(0, 0, 1D), new GeoPosition(0, 1, 1D),
+ new GeoPosition(1, 1, 1D), new GeoPosition(0, 0, 1D)
+ )));
+ BiFunction, GeoPolygon> polygonSupplier =
+ (box, properties) -> new GeoPolygon(rings, box, properties);
+
+ BiFunction, GeoPointCollection> multiPointSupplier =
+ (box, properties) -> new GeoPointCollection(Arrays.asList(pointSupplier.apply(null, null),
+ pointSupplier.apply(null, null)), box, properties);
+
+ BiFunction, GeoLineStringCollection> multiLineSupplier =
+ (box, properties) -> new GeoLineStringCollection(Arrays.asList(lineSupplier.apply(null, null),
+ lineSupplier.apply(null, null)), box, properties);
+
+ BiFunction, GeoPolygonCollection> multiPolygonSuppluer =
+ (box, properties) -> new GeoPolygonCollection(Arrays.asList(polygonSupplier.apply(null, null),
+ polygonSupplier.apply(null, null)), box, properties);
+
+ BiFunction, GeoCollection> collectionSupplier =
+ (box, properties) -> new GeoCollection(Arrays.asList(pointSupplier.apply(null, null),
+ multiLineSupplier.apply(box, properties), polygonSupplier.apply(box, properties)), box, properties);
+
+ return Stream.of(
+ // GeoPoint
+ Arguments.of(deserializerArgumentSupplier(null, null, pointSupplier)),
+ Arguments.of(deserializerArgumentSupplier(boundingBox, simpleProperties, pointSupplier)),
+ Arguments.of(deserializerArgumentSupplier(boundingBox, arrayProperties, pointSupplier)),
+ Arguments.of(deserializerArgumentSupplier(boundingBox, objectProperties, pointSupplier)),
+
+ // GeoLine
+ Arguments.of(deserializerArgumentSupplier(null, null, lineSupplier)),
+ Arguments.of(deserializerArgumentSupplier(boundingBox, simpleProperties, lineSupplier)),
+ Arguments.of(deserializerArgumentSupplier(boundingBox, arrayProperties, lineSupplier)),
+ Arguments.of(deserializerArgumentSupplier(boundingBox, objectProperties, lineSupplier)),
+
+ // GeoPolygon
+ Arguments.of(deserializerArgumentSupplier(null, null, polygonSupplier)),
+ Arguments.of(deserializerArgumentSupplier(boundingBox, simpleProperties, polygonSupplier)),
+ Arguments.of(deserializerArgumentSupplier(boundingBox, arrayProperties, polygonSupplier)),
+ Arguments.of(deserializerArgumentSupplier(boundingBox, objectProperties, polygonSupplier)),
+
+ // GeoPointCollection
+ Arguments.of(deserializerArgumentSupplier(null, null, multiPointSupplier)),
+ Arguments.of(deserializerArgumentSupplier(boundingBox, simpleProperties, multiPointSupplier)),
+ Arguments.of(deserializerArgumentSupplier(boundingBox, arrayProperties, multiPointSupplier)),
+ Arguments.of(deserializerArgumentSupplier(boundingBox, objectProperties, multiPointSupplier)),
+
+ // GeoLineCollection
+ Arguments.of(deserializerArgumentSupplier(null, null, multiLineSupplier)),
+ Arguments.of(deserializerArgumentSupplier(boundingBox, simpleProperties, multiLineSupplier)),
+ Arguments.of(deserializerArgumentSupplier(boundingBox, arrayProperties, multiLineSupplier)),
+ Arguments.of(deserializerArgumentSupplier(boundingBox, objectProperties, multiLineSupplier)),
+
+ // GeoPolygonCollection
+ Arguments.of(deserializerArgumentSupplier(null, null, multiPolygonSuppluer)),
+ Arguments.of(deserializerArgumentSupplier(boundingBox, simpleProperties, multiPolygonSuppluer)),
+ Arguments.of(deserializerArgumentSupplier(boundingBox, arrayProperties, multiPolygonSuppluer)),
+ Arguments.of(deserializerArgumentSupplier(boundingBox, objectProperties, multiPolygonSuppluer)),
+
+ // GeoCollection
+ Arguments.of(deserializerArgumentSupplier(null, null, collectionSupplier)),
+ Arguments.of(deserializerArgumentSupplier(boundingBox, simpleProperties, collectionSupplier)),
+ Arguments.of(deserializerArgumentSupplier(boundingBox, arrayProperties, collectionSupplier)),
+ Arguments.of(deserializerArgumentSupplier(boundingBox, objectProperties, collectionSupplier))
+ );
+ }
+
+ private static Object[] deserializerArgumentSupplier(GeoBoundingBox boundingBox,
+ Map properties,
+ BiFunction, ? extends GeoObject> geoSupplier) {
+ GeoObject geoObject = geoSupplier.apply(boundingBox, properties);
+ return new Object[]{GeoSerializationTestHelpers.geoToJson(geoObject), geoObject.getClass(), geoObject};
+ }
+
+ @Test
+ public void unknownGeoTypeSerializationThrows() {
+ Assertions.assertThrows(IOException.class,
+ () -> ADAPTER.serialize(new CustomGeoObject(null, null), SerializerEncoding.JSON));
+ }
+
+ private static final class CustomGeoObject extends GeoObject {
+ protected CustomGeoObject(GeoBoundingBox boundingBox, Map properties) {
+ super(boundingBox, properties);
+ }
+
+ @Override
+ public GeoObjectType getType() {
+ return null;
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("geoJsonSerializationSupplier")
+ public void geoJsonSerialization(T geo, String expectedJson) throws IOException {
+ String actualJson = ADAPTER.serialize(geo, SerializerEncoding.JSON);
+ Assertions.assertEquals(expectedJson, actualJson);
+ }
+
+ private static Stream geoJsonSerializationSupplier() {
+ GeoBoundingBox boundingBox = new GeoBoundingBox(0, 0, 1, 1, 0D, 1D);
+ Map simpleProperties = Collections.singletonMap("key", "value");
+ Map arrayProperties = Collections.singletonMap("text", Arrays.asList("hello", "world"));
+
+ Map crs = new HashMap<>();
+ crs.put("type", "name");
+ crs.put("properties", Collections.singletonMap("name", "EPSG:432"));
+ Map objectProperties = Collections.singletonMap("crs", crs);
+
+ BiFunction, GeoPoint> pointSupplier =
+ (box, properties) -> new GeoPoint(new GeoPosition(0, 0, 0D), box, properties);
+
+ List positions = Arrays.asList(new GeoPosition(0, 0, 1D),
+ new GeoPosition(1, 1, 1D));
+ BiFunction, GeoLineString> lineSupplier =
+ (box, properties) -> new GeoLineString(positions, box, properties);
+
+ List rings = Collections.singletonList(new GeoLinearRing(Arrays.asList(
+ new GeoPosition(0, 0, 1D), new GeoPosition(0, 1, 1D),
+ new GeoPosition(1, 1, 1D), new GeoPosition(0, 0, 1D)
+ )));
+ BiFunction, GeoPolygon> polygonSupplier =
+ (box, properties) -> new GeoPolygon(rings, box, properties);
+
+ BiFunction, GeoPointCollection> multiPointSupplier =
+ (box, properties) -> new GeoPointCollection(Arrays.asList(pointSupplier.apply(null, null),
+ pointSupplier.apply(box, properties)), box, properties);
+
+ BiFunction, GeoLineStringCollection> multiLineSupplier =
+ (box, properties) -> new GeoLineStringCollection(Arrays.asList(lineSupplier.apply(null, null),
+ lineSupplier.apply(box, properties)), box, properties);
+
+ BiFunction, GeoPolygonCollection> multiPolygonSuppluer =
+ (box, properties) -> new GeoPolygonCollection(Arrays.asList(polygonSupplier.apply(null, null),
+ polygonSupplier.apply(box, properties)), box, properties);
+
+ BiFunction, GeoCollection> collectionSupplier =
+ (box, properties) -> new GeoCollection(Arrays.asList(pointSupplier.apply(null, null),
+ multiPointSupplier.apply(box, properties)), box, properties);
+
+ return Stream.of(
+ // GeoPoint
+ Arguments.of(serializerArgumentSupplier(null, null, pointSupplier)),
+ Arguments.of(serializerArgumentSupplier(boundingBox, simpleProperties, pointSupplier)),
+ Arguments.of(serializerArgumentSupplier(boundingBox, arrayProperties, pointSupplier)),
+ Arguments.of(serializerArgumentSupplier(boundingBox, objectProperties, pointSupplier)),
+
+ // GeoLine
+ Arguments.of(serializerArgumentSupplier(null, null, lineSupplier)),
+ Arguments.of(serializerArgumentSupplier(boundingBox, simpleProperties, lineSupplier)),
+ Arguments.of(serializerArgumentSupplier(boundingBox, arrayProperties, lineSupplier)),
+ Arguments.of(serializerArgumentSupplier(boundingBox, objectProperties, lineSupplier)),
+
+ // GeoPolygon
+ Arguments.of(serializerArgumentSupplier(null, null, polygonSupplier)),
+ Arguments.of(serializerArgumentSupplier(boundingBox, simpleProperties, polygonSupplier)),
+ Arguments.of(serializerArgumentSupplier(boundingBox, arrayProperties, polygonSupplier)),
+ Arguments.of(serializerArgumentSupplier(boundingBox, objectProperties, polygonSupplier)),
+
+ // GeoPointCollection
+ Arguments.of(serializerArgumentSupplier(null, null, multiPointSupplier)),
+ Arguments.of(serializerArgumentSupplier(boundingBox, simpleProperties, multiPointSupplier)),
+ Arguments.of(serializerArgumentSupplier(boundingBox, arrayProperties, multiPointSupplier)),
+ Arguments.of(serializerArgumentSupplier(boundingBox, objectProperties, multiPointSupplier)),
+
+ // GeoLineCollection
+ Arguments.of(serializerArgumentSupplier(null, null, multiLineSupplier)),
+ Arguments.of(serializerArgumentSupplier(boundingBox, simpleProperties, multiLineSupplier)),
+ Arguments.of(serializerArgumentSupplier(boundingBox, arrayProperties, multiLineSupplier)),
+ Arguments.of(serializerArgumentSupplier(boundingBox, objectProperties, multiLineSupplier)),
+
+ // GeoPolygonCollection
+ Arguments.of(serializerArgumentSupplier(null, null, multiPolygonSuppluer)),
+ Arguments.of(serializerArgumentSupplier(boundingBox, simpleProperties, multiPolygonSuppluer)),
+ Arguments.of(serializerArgumentSupplier(boundingBox, arrayProperties, multiPolygonSuppluer)),
+ Arguments.of(serializerArgumentSupplier(boundingBox, objectProperties, multiPolygonSuppluer)),
+
+ // GeoCollection
+ Arguments.of(serializerArgumentSupplier(null, null, collectionSupplier)),
+ Arguments.of(serializerArgumentSupplier(boundingBox, simpleProperties, collectionSupplier)),
+ Arguments.of(serializerArgumentSupplier(boundingBox, arrayProperties, collectionSupplier)),
+ Arguments.of(serializerArgumentSupplier(boundingBox, objectProperties, collectionSupplier))
+ );
+ }
+
+ private static Object[] serializerArgumentSupplier(GeoBoundingBox boundingBox, Map properties,
+ BiFunction, ? extends GeoObject> geoSupplier) {
+ GeoObject geoObject = geoSupplier.apply(boundingBox, properties);
+ return new Object[]{geoObject, GeoSerializationTestHelpers.geoToJson(geoObject)};
+ }
+}
diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/FlatteningSerializerTests.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/FlatteningSerializerTests.java
new file mode 100644
index 0000000000000..d1ef2bcad726e
--- /dev/null
+++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/FlatteningSerializerTests.java
@@ -0,0 +1,765 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.version.tests;
+
+
+import com.azure.core.implementation.TypeUtil;
+import com.azure.core.util.serializer.JacksonAdapter;
+import com.azure.core.util.serializer.SerializerEncoding;
+import com.azure.core.version.tests.models.AnimalShelter;
+import com.azure.core.version.tests.models.AnimalWithTypeIdContainingDot;
+import com.azure.core.version.tests.models.ClassWithFlattenedProperties;
+import com.azure.core.version.tests.models.ComposeTurtles;
+import com.azure.core.version.tests.models.DogWithTypeIdContainingDot;
+import com.azure.core.version.tests.models.FlattenDangling;
+import com.azure.core.version.tests.models.FlattenableAnimalInfo;
+import com.azure.core.version.tests.models.FlattenedProduct;
+import com.azure.core.version.tests.models.FlattenedPropertiesAndJsonAnyGetter;
+import com.azure.core.version.tests.models.Foo;
+import com.azure.core.version.tests.models.JsonFlattenNestedInner;
+import com.azure.core.version.tests.models.JsonFlattenOnArrayType;
+import com.azure.core.version.tests.models.JsonFlattenOnCollectionType;
+import com.azure.core.version.tests.models.JsonFlattenOnJsonIgnoredProperty;
+import com.azure.core.version.tests.models.JsonFlattenOnPrimitiveType;
+import com.azure.core.version.tests.models.JsonFlattenWithJsonInfoDiscriminator;
+import com.azure.core.version.tests.models.RabbitWithTypeIdContainingDot;
+import com.azure.core.version.tests.models.SampleResource;
+import com.azure.core.version.tests.models.School;
+import com.azure.core.version.tests.models.Student;
+import com.azure.core.version.tests.models.Teacher;
+import com.azure.core.version.tests.models.TurtleWithTypeIdContainingDot;
+import com.azure.core.version.tests.models.VirtualMachineIdentity;
+import com.azure.core.version.tests.models.VirtualMachineScaleSet;
+import com.azure.core.version.tests.models.VirtualMachineScaleSetNetworkConfiguration;
+import com.azure.core.version.tests.models.VirtualMachineScaleSetNetworkProfile;
+import com.azure.core.version.tests.models.VirtualMachineScaleSetVMProfile;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class FlatteningSerializerTests {
+ private static final JacksonAdapter ADAPTER = new JacksonAdapter();
+
+ @Test
+ public void canFlatten() {
+ Foo foo = new Foo();
+ foo.bar("hello.world");
+ //
+ List baz = Arrays.asList("hello", "hello.world");
+ foo.baz(baz);
+
+ HashMap qux = new HashMap<>();
+ qux.put("hello", "world");
+ qux.put("a.b", "c.d");
+ qux.put("bar.a", "ttyy");
+ qux.put("bar.b", "uuzz");
+ foo.qux(qux);
+
+ // serialization
+ String serialized = serialize(foo);
+ assertEquals("{\"$type\":\"foo\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}}}", serialized);
+
+ // deserialization
+ Foo deserialized = deserialize(serialized, Foo.class);
+ assertEquals("hello.world", deserialized.bar());
+ Assertions.assertArrayEquals(new String[]{"hello", "hello.world"}, deserialized.baz().toArray());
+ assertNotNull(deserialized.qux());
+ assertEquals("world", deserialized.qux().get("hello"));
+ assertEquals("c.d", deserialized.qux().get("a.b"));
+ assertEquals("ttyy", deserialized.qux().get("bar.a"));
+ assertEquals("uuzz", deserialized.qux().get("bar.b"));
+ }
+
+ @Test
+ public void canSerializeMapKeysWithDotAndSlash() {
+ Teacher teacher = new Teacher();
+
+ Map students = new HashMap<>();
+ students.put("af.B/C", new Student());
+ students.put("af.B/D", new Student());
+
+ teacher.setStudents(students);
+
+ School school = new School().setName("school1");
+ school.setTeacher(teacher);
+
+ Map schoolTags = new HashMap<>();
+ schoolTags.put("foo.aa", "bar");
+ schoolTags.put("x.y", "zz");
+
+ school.setTags(schoolTags);
+
+ String serialized = serialize(school);
+ assertEquals("{\"teacher\":{\"students\":{\"af.B/D\":{},\"af.B/C\":{}}},\"tags\":{\"foo.aa\":\"bar\",\"x.y\":\"zz\"},\"properties\":{\"name\":\"school1\"}}", serialized);
+ }
+
+ /**
+ * Validates decoding and encoding of a type with type id containing dot and no additional properties For decoding
+ * and encoding base type will be used.
+ */
+ @Test
+ public void canHandleTypeWithTypeIdContainingDotAndNoProperties() {
+ String rabbitSerialized = "{\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\"}";
+ String shelterSerialized = "{\"properties\":{\"animalsInfo\":[{\"animal\":{\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\"}},{\"animal\":{\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\"}}]}}";
+
+ AnimalWithTypeIdContainingDot rabbitDeserialized = deserialize(rabbitSerialized,
+ AnimalWithTypeIdContainingDot.class);
+ assertTrue(rabbitDeserialized instanceof RabbitWithTypeIdContainingDot);
+ assertNotNull(rabbitDeserialized);
+
+ AnimalShelter shelterDeserialized = deserialize(shelterSerialized, AnimalShelter.class);
+ assertNotNull(shelterDeserialized);
+ assertEquals(2, shelterDeserialized.animalsInfo().size());
+ for (FlattenableAnimalInfo animalInfo : shelterDeserialized.animalsInfo()) {
+ assertTrue(animalInfo.animal() instanceof RabbitWithTypeIdContainingDot);
+ assertNotNull(animalInfo.animal());
+ }
+ }
+
+ /**
+ * Validates that decoding and encoding of a type with type id containing dot and can be done. For decoding and
+ * encoding base type will be used.
+ */
+ @Test
+ public void canHandleTypeWithTypeIdContainingDot0() {
+ List meals = Arrays.asList("carrot", "apple");
+ //
+ AnimalWithTypeIdContainingDot animalToSerialize = new RabbitWithTypeIdContainingDot().withMeals(meals);
+ String serialized = serialize(animalToSerialize);
+ //
+ String[] results = {
+ "{\"meals\":[\"carrot\",\"apple\"],\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\"}",
+ "{\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\",\"meals\":[\"carrot\",\"apple\"]}"
+ };
+
+ assertTrue(Arrays.asList(results).contains(serialized));
+
+ // De-Serialize
+ //
+ AnimalWithTypeIdContainingDot animalDeserialized = deserialize(serialized, AnimalWithTypeIdContainingDot.class);
+ assertTrue(animalDeserialized instanceof RabbitWithTypeIdContainingDot);
+ RabbitWithTypeIdContainingDot rabbit = (RabbitWithTypeIdContainingDot) animalDeserialized;
+ assertNotNull(rabbit.meals());
+ assertEquals(rabbit.meals().size(), 2);
+ }
+
+ /**
+ * Validates that decoding and encoding of a type with type id containing dot and can be done. For decoding and
+ * encoding concrete type will be used.
+ */
+ @Test
+ public void canHandleTypeWithTypeIdContainingDot1() {
+ List meals = Arrays.asList("carrot", "apple");
+ //
+ RabbitWithTypeIdContainingDot rabbitToSerialize = new RabbitWithTypeIdContainingDot().withMeals(meals);
+ String serialized = serialize(rabbitToSerialize);
+ //
+ String[] results = {
+ "{\"meals\":[\"carrot\",\"apple\"],\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\"}",
+ "{\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\",\"meals\":[\"carrot\",\"apple\"]}"
+ };
+
+ assertTrue(Arrays.asList(results).contains(serialized));
+
+ // De-Serialize
+ //
+ RabbitWithTypeIdContainingDot rabbitDeserialized = deserialize(serialized, RabbitWithTypeIdContainingDot.class);
+ assertNotNull(rabbitDeserialized);
+ assertNotNull(rabbitDeserialized.meals());
+ assertEquals(rabbitDeserialized.meals().size(), 2);
+ }
+
+
+ /**
+ * Validates that decoding and encoding of a type with flattenable property and type id containing dot and can be
+ * done. For decoding and encoding base type will be used.
+ */
+ @Test
+ public void canHandleTypeWithFlattenablePropertyAndTypeIdContainingDot0() {
+ AnimalWithTypeIdContainingDot animalToSerialize = new DogWithTypeIdContainingDot()
+ .withBreed("AKITA")
+ .withCuteLevel(10);
+
+ // serialization
+ String serialized = serialize(animalToSerialize);
+ String[] results = {
+ "{\"breed\":\"AKITA\",\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\",\"properties\":{\"cuteLevel\":10}}",
+ "{\"breed\":\"AKITA\",\"properties\":{\"cuteLevel\":10},\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\"}",
+ "{\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\",\"breed\":\"AKITA\",\"properties\":{\"cuteLevel\":10}}",
+ "{\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\",\"properties\":{\"cuteLevel\":10},\"breed\":\"AKITA\"}",
+ "{\"properties\":{\"cuteLevel\":10},\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\",\"breed\":\"AKITA\"}",
+ "{\"properties\":{\"cuteLevel\":10},\"breed\":\"AKITA\",\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\"}",
+ };
+
+ assertTrue(Arrays.asList(results).contains(serialized));
+
+ // de-serialization
+ AnimalWithTypeIdContainingDot animalDeserialized = deserialize(serialized, AnimalWithTypeIdContainingDot.class);
+ assertTrue(animalDeserialized instanceof DogWithTypeIdContainingDot);
+ DogWithTypeIdContainingDot dogDeserialized = (DogWithTypeIdContainingDot) animalDeserialized;
+ assertNotNull(dogDeserialized);
+ assertEquals(dogDeserialized.breed(), "AKITA");
+ assertEquals(dogDeserialized.cuteLevel(), (Integer) 10);
+ }
+
+ /**
+ * Validates that decoding and encoding of a type with flattenable property and type id containing dot and can be
+ * done. For decoding and encoding concrete type will be used.
+ */
+ @Test
+ public void canHandleTypeWithFlattenablePropertyAndTypeIdContainingDot1() {
+ DogWithTypeIdContainingDot dogToSerialize = new DogWithTypeIdContainingDot().withBreed("AKITA").withCuteLevel(10);
+
+ // serialization
+ String serialized = serialize(dogToSerialize);
+ String[] results = {
+ "{\"breed\":\"AKITA\",\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\",\"properties\":{\"cuteLevel\":10}}",
+ "{\"breed\":\"AKITA\",\"properties\":{\"cuteLevel\":10},\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\"}",
+ "{\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\",\"breed\":\"AKITA\",\"properties\":{\"cuteLevel\":10}}",
+ "{\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\",\"properties\":{\"cuteLevel\":10},\"breed\":\"AKITA\"}",
+ "{\"properties\":{\"cuteLevel\":10},\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\",\"breed\":\"AKITA\"}",
+ "{\"properties\":{\"cuteLevel\":10},\"breed\":\"AKITA\",\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\"}",
+ };
+
+ assertTrue(Arrays.asList(results).contains(serialized));
+
+ // de-serialization
+ DogWithTypeIdContainingDot dogDeserialized = deserialize(serialized, DogWithTypeIdContainingDot.class);
+ assertNotNull(dogDeserialized);
+ assertEquals(dogDeserialized.breed(), "AKITA");
+ assertEquals(dogDeserialized.cuteLevel(), (Integer) 10);
+ }
+
+ /**
+ * Validates that decoding and encoding of a array of type with type id containing dot and can be done. For decoding
+ * and encoding base type will be used.
+ */
+ @Test
+ public void canHandleArrayOfTypeWithTypeIdContainingDot0() {
+ List meals = Arrays.asList("carrot", "apple");
+ //
+ AnimalWithTypeIdContainingDot animalToSerialize = new RabbitWithTypeIdContainingDot().withMeals(meals);
+ List animalsToSerialize = new ArrayList<>();
+ animalsToSerialize.add(animalToSerialize);
+ String serialized = serialize(animalsToSerialize);
+ String[] results = {
+ "[{\"meals\":[\"carrot\",\"apple\"],\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\"}]",
+ "[{\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\",\"meals\":[\"carrot\",\"apple\"]}]",
+ };
+
+ assertTrue(Arrays.asList(results).contains(serialized));
+
+ // De-serialize
+ //
+ List animalsDeserialized = deserialize(serialized,
+ TypeUtil.createParameterizedType(List.class, AnimalWithTypeIdContainingDot.class));
+ assertNotNull(animalsDeserialized);
+ assertEquals(1, animalsDeserialized.size());
+ AnimalWithTypeIdContainingDot animalDeserialized = animalsDeserialized.get(0);
+ assertTrue(animalDeserialized instanceof RabbitWithTypeIdContainingDot);
+ RabbitWithTypeIdContainingDot rabbitDeserialized = (RabbitWithTypeIdContainingDot) animalDeserialized;
+ assertNotNull(rabbitDeserialized.meals());
+ assertEquals(rabbitDeserialized.meals().size(), 2);
+ }
+
+ /**
+ * Validates that decoding and encoding of a array of type with type id containing dot and can be done. For decoding
+ * and encoding concrete type will be used.
+ */
+ @Test
+ public void canHandleArrayOfTypeWithTypeIdContainingDot1() {
+ List meals = Arrays.asList("carrot", "apple");
+ //
+ RabbitWithTypeIdContainingDot rabbitToSerialize = new RabbitWithTypeIdContainingDot().withMeals(meals);
+ List rabbitsToSerialize = new ArrayList<>();
+ rabbitsToSerialize.add(rabbitToSerialize);
+ String serialized = serialize(rabbitsToSerialize);
+ String[] results = {
+ "[{\"meals\":[\"carrot\",\"apple\"],\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\"}]",
+ "[{\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\",\"meals\":[\"carrot\",\"apple\"]}]",
+ };
+
+ assertTrue(Arrays.asList(results).contains(serialized));
+
+ // De-serialize
+ //
+ List rabbitsDeserialized = deserialize(serialized,
+ TypeUtil.createParameterizedType(List.class, RabbitWithTypeIdContainingDot.class));
+ assertNotNull(rabbitsDeserialized);
+ assertEquals(1, rabbitsDeserialized.size());
+ RabbitWithTypeIdContainingDot rabbitDeserialized = rabbitsDeserialized.get(0);
+ assertNotNull(rabbitDeserialized.meals());
+ assertEquals(rabbitDeserialized.meals().size(), 2);
+ }
+
+
+ /**
+ * Validates that decoding and encoding of a composed type with type id containing dot and can be done.
+ */
+ @Test
+ public void canHandleComposedTypeWithTypeIdContainingDot0() {
+ List meals = Arrays.asList("carrot", "apple");
+ AnimalWithTypeIdContainingDot animalToSerialize = new RabbitWithTypeIdContainingDot().withMeals(meals);
+ FlattenableAnimalInfo animalInfoToSerialize = new FlattenableAnimalInfo().withAnimal(animalToSerialize);
+ List animalsInfoSerialized = Collections.singletonList(animalInfoToSerialize);
+ AnimalShelter animalShelterToSerialize = new AnimalShelter().withAnimalsInfo(animalsInfoSerialized);
+ String serialized = serialize(animalShelterToSerialize);
+ String[] results = {
+ "{\"properties\":{\"animalsInfo\":[{\"animal\":{\"meals\":[\"carrot\",\"apple\"],\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\"}}]}}",
+ "{\"properties\":{\"animalsInfo\":[{\"animal\":{\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\",\"meals\":[\"carrot\",\"apple\"]}}]}}",
+ };
+
+ assertTrue(Arrays.asList(results).contains(serialized));
+
+ // de-serialization
+ //
+ AnimalShelter shelterDeserialized = deserialize(serialized, AnimalShelter.class);
+ assertNotNull(shelterDeserialized.animalsInfo());
+ assertEquals(shelterDeserialized.animalsInfo().size(), 1);
+ FlattenableAnimalInfo animalsInfoDeserialized = shelterDeserialized.animalsInfo().get(0);
+ assertTrue(animalsInfoDeserialized.animal() instanceof RabbitWithTypeIdContainingDot);
+ AnimalWithTypeIdContainingDot animalDeserialized = animalsInfoDeserialized.animal();
+ assertTrue(animalDeserialized instanceof RabbitWithTypeIdContainingDot);
+ RabbitWithTypeIdContainingDot rabbitDeserialized = (RabbitWithTypeIdContainingDot) animalDeserialized;
+ assertNotNull(rabbitDeserialized);
+ assertNotNull(rabbitDeserialized.meals());
+ assertEquals(rabbitDeserialized.meals().size(), 2);
+ }
+
+ @Test
+ public void canHandleComposedSpecificPolymorphicTypeWithTypeId() {
+ //
+ // -- Validate vector property
+ //
+ String serializedCollectionWithTypeId = "{\"turtlesSet1\":[{\"age\":100,\"size\":10,\"@odata.type\":\"#Favourite.Pet.TurtleWithTypeIdContainingDot\"},{\"age\":200,\"size\":20,\"@odata.type\":\"#Favourite.Pet.TurtleWithTypeIdContainingDot\"}]}";
+ // de-serialization
+ //
+ ComposeTurtles composedTurtleDeserialized = deserialize(serializedCollectionWithTypeId, ComposeTurtles.class);
+ assertNotNull(composedTurtleDeserialized);
+ assertNotNull(composedTurtleDeserialized.turtlesSet1());
+ assertEquals(2, composedTurtleDeserialized.turtlesSet1().size());
+ //
+ serialize(composedTurtleDeserialized);
+ //
+ // -- Validate scalar property
+ //
+ String serializedScalarWithTypeId = "{\"turtlesSet1Lead\":{\"age\":100,\"size\":10,\"@odata.type\":\"#Favourite.Pet.TurtleWithTypeIdContainingDot\"}}";
+ // de-serialization
+ //
+ composedTurtleDeserialized = deserialize(serializedScalarWithTypeId, ComposeTurtles.class);
+ assertNotNull(composedTurtleDeserialized);
+ assertNotNull(composedTurtleDeserialized.turtlesSet1Lead());
+ assertEquals(10, (long) composedTurtleDeserialized.turtlesSet1Lead().size());
+ assertEquals(100, (long) composedTurtleDeserialized.turtlesSet1Lead().age());
+ //
+ serialize(composedTurtleDeserialized);
+ }
+
+ @Test
+ public void canHandleComposedSpecificPolymorphicTypeWithoutTypeId() {
+ //
+ // -- Validate vector property
+ //
+ String serializedCollectionWithTypeId = "{\"turtlesSet1\":[{\"age\":100,\"size\":10 },{\"age\":200,\"size\":20 }]}";
+ // de-serialization
+ //
+ ComposeTurtles composedTurtleDeserialized = deserialize(serializedCollectionWithTypeId, ComposeTurtles.class);
+ assertNotNull(composedTurtleDeserialized);
+ assertNotNull(composedTurtleDeserialized.turtlesSet1());
+ assertEquals(2, composedTurtleDeserialized.turtlesSet1().size());
+ //
+ serialize(composedTurtleDeserialized);
+ //
+ // -- Validate scalar property
+ //
+ String serializedScalarWithTypeId = "{\"turtlesSet1Lead\":{\"age\":100,\"size\":10 }}";
+ // de-serialization
+ //
+ composedTurtleDeserialized = deserialize(serializedScalarWithTypeId, ComposeTurtles.class);
+ assertNotNull(composedTurtleDeserialized);
+ assertNotNull(composedTurtleDeserialized.turtlesSet1Lead());
+ assertEquals(100, (long) composedTurtleDeserialized.turtlesSet1Lead().age());
+ //
+ serialize(composedTurtleDeserialized);
+ }
+
+ @Test
+ public void canHandleComposedSpecificPolymorphicTypeWithAndWithoutTypeId() {
+ //
+ // -- Validate vector property
+ //
+ String serializedCollectionWithTypeId = "{\"turtlesSet1\":[{\"age\":100,\"size\":10,\"@odata.type\":\"#Favourite.Pet.TurtleWithTypeIdContainingDot\"},{\"age\":200,\"size\":20 }]}";
+ // de-serialization
+ //
+ ComposeTurtles composedTurtleDeserialized = deserialize(serializedCollectionWithTypeId, ComposeTurtles.class);
+ assertNotNull(composedTurtleDeserialized);
+ assertNotNull(composedTurtleDeserialized.turtlesSet1());
+ assertEquals(2, composedTurtleDeserialized.turtlesSet1().size());
+ //
+ serialize(composedTurtleDeserialized);
+ }
+
+ @Test
+ public void canHandleComposedGenericPolymorphicTypeWithTypeId() {
+ //
+ // -- Validate vector property
+ //
+ String serializedCollectionWithTypeId = "{\"turtlesSet2\":[{\"age\":100,\"size\":10,\"@odata.type\":\"#Favourite.Pet.TurtleWithTypeIdContainingDot\"},{\"age\":200,\"size\":20,\"@odata.type\":\"#Favourite.Pet.TurtleWithTypeIdContainingDot\"}]}";
+ // de-serialization
+ //
+ ComposeTurtles composedTurtleDeserialized = deserialize(serializedCollectionWithTypeId, ComposeTurtles.class);
+ assertNotNull(composedTurtleDeserialized);
+ assertNotNull(composedTurtleDeserialized.turtlesSet2());
+ assertEquals(2, composedTurtleDeserialized.turtlesSet2().size());
+ //
+ assertTrue(composedTurtleDeserialized.turtlesSet2().get(0) instanceof TurtleWithTypeIdContainingDot);
+ assertTrue(composedTurtleDeserialized.turtlesSet2().get(1) instanceof TurtleWithTypeIdContainingDot);
+ //
+ serialize(composedTurtleDeserialized);
+ //
+ // -- Validate scalar property
+ //
+ String serializedScalarWithTypeId = "{\"turtlesSet2Lead\":{\"age\":100,\"size\":10,\"@odata.type\":\"#Favourite.Pet.TurtleWithTypeIdContainingDot\"}}";
+ // de-serialization
+ //
+ composedTurtleDeserialized = deserialize(serializedScalarWithTypeId, ComposeTurtles.class);
+ assertNotNull(composedTurtleDeserialized);
+ assertNotNull(composedTurtleDeserialized.turtlesSet2Lead());
+ assertTrue(composedTurtleDeserialized.turtlesSet2Lead() instanceof TurtleWithTypeIdContainingDot);
+ assertEquals(10, (long) ((TurtleWithTypeIdContainingDot) composedTurtleDeserialized.turtlesSet2Lead()).size());
+ assertEquals(100, (long) composedTurtleDeserialized.turtlesSet2Lead().age());
+ //
+ serialize(composedTurtleDeserialized);
+ }
+
+ @Test
+ public void canHandleComposedGenericPolymorphicTypeWithoutTypeId() {
+ //
+ // -- Validate vector property
+ //
+ String serializedCollectionWithTypeId = "{\"turtlesSet2\":[{\"age\":100,\"size\":10 },{\"age\":200,\"size\":20 }]}";
+ // de-serialization
+ //
+ ComposeTurtles composedTurtleDeserialized = deserialize(serializedCollectionWithTypeId, ComposeTurtles.class);
+ assertNotNull(composedTurtleDeserialized);
+ assertNotNull(composedTurtleDeserialized.turtlesSet2());
+ assertEquals(2, composedTurtleDeserialized.turtlesSet2().size());
+ //
+ Assertions.assertFalse(composedTurtleDeserialized.turtlesSet2().get(0) instanceof TurtleWithTypeIdContainingDot);
+ Assertions.assertFalse(composedTurtleDeserialized.turtlesSet2().get(1) instanceof TurtleWithTypeIdContainingDot);
+ //
+ // -- Validate scalar property
+ //
+ serialize(composedTurtleDeserialized);
+ //
+ String serializedScalarWithTypeId = "{\"turtlesSet2Lead\":{\"age\":100,\"size\":10 }}";
+ // de-serialization
+ //
+ composedTurtleDeserialized = deserialize(serializedScalarWithTypeId, ComposeTurtles.class);
+ assertNotNull(composedTurtleDeserialized);
+ assertNotNull(composedTurtleDeserialized.turtlesSet2Lead());
+ //
+ serialize(composedTurtleDeserialized);
+ }
+
+ @Test
+ public void canHandleComposedGenericPolymorphicTypeWithAndWithoutTypeId() {
+ //
+ // -- Validate vector property
+ //
+ String serializedCollectionWithTypeId = "{\"turtlesSet2\":[{\"age\":100,\"size\":10,\"@odata.type\":\"#Favourite.Pet.TurtleWithTypeIdContainingDot\"},{\"age\":200,\"size\":20 }]}";
+ // de-serialization
+ //
+ ComposeTurtles composedTurtleDeserialized = deserialize(serializedCollectionWithTypeId, ComposeTurtles.class);
+ assertNotNull(composedTurtleDeserialized);
+ assertNotNull(composedTurtleDeserialized.turtlesSet2());
+ assertEquals(2, composedTurtleDeserialized.turtlesSet2().size());
+ //
+ assertTrue(composedTurtleDeserialized.turtlesSet2().get(0) instanceof TurtleWithTypeIdContainingDot);
+ assertNotNull(composedTurtleDeserialized.turtlesSet2().get(1));
+ //
+ serialize(composedTurtleDeserialized);
+ }
+
+ @Test
+ public void canHandleEscapedProperties() {
+ FlattenedProduct productToSerialize = new FlattenedProduct()
+ .setProductName("drink")
+ .setProductType("chai");
+
+ // serialization
+ //
+ String serialized = serialize(productToSerialize);
+ String[] results = {
+ "{\"properties\":{\"p.name\":\"drink\",\"type\":\"chai\"}}",
+ "{\"properties\":{\"type\":\"chai\",\"p.name\":\"drink\"}}",
+ };
+
+ assertTrue(Arrays.asList(results).contains(serialized));
+
+ // de-serialization
+ //
+ FlattenedProduct productDeserialized = deserialize(serialized, FlattenedProduct.class);
+ assertNotNull(productDeserialized);
+ assertEquals(productDeserialized.getProductName(), "drink");
+ assertEquals(productDeserialized.getProductType(), "chai");
+ }
+
+ @Test
+ public void canHandleSinglePropertyBeingFlattened() {
+ ClassWithFlattenedProperties classWithFlattenedProperties = new ClassWithFlattenedProperties("random", "E24JJxztP");
+
+ String serialized = serialize(classWithFlattenedProperties);
+ String[] results = {
+ "{\"@odata\":{\"type\":\"random\"},\"@odata.etag\":\"E24JJxztP\"}",
+ "{\"@odata.etag\":\"E24JJxztP\",\"@odata\":{\"type\":\"random\"}}"
+ };
+
+ assertTrue(Arrays.asList(results).contains(serialized));
+
+ ClassWithFlattenedProperties deserialized = deserialize(serialized, ClassWithFlattenedProperties.class);
+ assertNotNull(deserialized);
+ assertEquals(classWithFlattenedProperties.getOdataType(), deserialized.getOdataType());
+ assertEquals(classWithFlattenedProperties.getOdataETag(), deserialized.getOdataETag());
+ }
+
+ @Test
+ public void canHandleMultiLevelPropertyFlattening() {
+ VirtualMachineScaleSet virtualMachineScaleSet = new VirtualMachineScaleSet()
+ .setVirtualMachineProfile(new VirtualMachineScaleSetVMProfile()
+ .setNetworkProfile(new VirtualMachineScaleSetNetworkProfile()
+ .setNetworkInterfaceConfigurations(Collections.singletonList(
+ new VirtualMachineScaleSetNetworkConfiguration().setName("name").setPrimary(true)))));
+
+ String serialized = serialize(virtualMachineScaleSet);
+ String expected = "{\"properties\":{\"virtualMachineProfile\":{\"networkProfile\":{\"networkInterfaceConfigurations\":[{\"name\":\"name\",\"properties\":{\"primary\":true}}]}}}}";
+ assertEquals(expected, serialized);
+
+ VirtualMachineScaleSet deserialized = deserialize(serialized, VirtualMachineScaleSet.class);
+ assertNotNull(deserialized);
+
+ VirtualMachineScaleSetNetworkConfiguration expectedConfig = virtualMachineScaleSet.getVirtualMachineProfile()
+ .getNetworkProfile()
+ .getNetworkInterfaceConfigurations()
+ .get(0);
+
+ VirtualMachineScaleSetNetworkConfiguration actualConfig = deserialized.getVirtualMachineProfile()
+ .getNetworkProfile()
+ .getNetworkInterfaceConfigurations()
+ .get(0);
+
+ assertEquals(expectedConfig.getName(), actualConfig.getName());
+ assertEquals(expectedConfig.getPrimary(), actualConfig.getPrimary());
+ }
+
+ @Test
+ public void jsonFlattenOnArrayType() {
+ JsonFlattenOnArrayType expected = new JsonFlattenOnArrayType()
+ .setJsonFlattenArray(new String[]{"hello", "goodbye", null});
+
+ String expectedSerialization = "{\"jsonflatten\":{\"array\":[\"hello\",\"goodbye\",null]}}";
+ String actualSerialization = serialize(expected);
+
+ assertEquals(expectedSerialization, actualSerialization);
+
+ JsonFlattenOnArrayType deserialized = deserialize(actualSerialization, JsonFlattenOnArrayType.class);
+ assertArrayEquals(expected.getJsonFlattenArray(), deserialized.getJsonFlattenArray());
+ }
+
+ @Test
+ public void jsonFlattenOnCollectionTypeList() {
+ final List listCollection = Arrays.asList("hello", "goodbye", null);
+ JsonFlattenOnCollectionType expected = new JsonFlattenOnCollectionType()
+ .setJsonFlattenCollection(Collections.unmodifiableList(listCollection));
+
+ String expectedSerialization = "{\"jsonflatten\":{\"collection\":[\"hello\",\"goodbye\",null]}}";
+ String actualSerialization = serialize(expected);
+
+ assertEquals(expectedSerialization, actualSerialization);
+
+ JsonFlattenOnCollectionType deserialized = deserialize(actualSerialization, JsonFlattenOnCollectionType.class);
+ assertEquals(expected.getJsonFlattenCollection().size(), deserialized.getJsonFlattenCollection().size());
+ for (int i = 0; i < expected.getJsonFlattenCollection().size(); i++) {
+ assertEquals(expected.getJsonFlattenCollection().get(i), deserialized.getJsonFlattenCollection().get(i));
+ }
+ }
+
+ @Test
+ public void jsonFlattenOnJsonIgnoredProperty() {
+ JsonFlattenOnJsonIgnoredProperty expected = new JsonFlattenOnJsonIgnoredProperty()
+ .setName("name")
+ .setIgnored("ignored");
+
+ String expectedSerialization = "{\"name\":\"name\"}";
+ String actualSerialization = serialize(expected);
+
+ assertEquals(expectedSerialization, actualSerialization);
+
+ JsonFlattenOnJsonIgnoredProperty deserialized = deserialize(actualSerialization,
+ JsonFlattenOnJsonIgnoredProperty.class);
+ assertEquals(expected.getName(), deserialized.getName());
+ assertNull(deserialized.getIgnored());
+ }
+
+ @Test
+ public void jsonFlattenOnPrimitiveType() {
+ JsonFlattenOnPrimitiveType expected = new JsonFlattenOnPrimitiveType()
+ .setJsonFlattenBoolean(true)
+ .setJsonFlattenDecimal(1.25D)
+ .setJsonFlattenNumber(2)
+ .setJsonFlattenString("string");
+
+ String expectedSerialization = "{\"jsonflatten\":{\"boolean\":true,\"decimal\":1.25,\"number\":2,\"string\":\"string\"}}";
+ String actualSerialization = serialize(expected);
+
+ assertEquals(expectedSerialization, actualSerialization);
+
+ JsonFlattenOnPrimitiveType deserialized = deserialize(actualSerialization, JsonFlattenOnPrimitiveType.class);
+ assertEquals(expected.isJsonFlattenBoolean(), deserialized.isJsonFlattenBoolean());
+ assertEquals(expected.getJsonFlattenDecimal(), deserialized.getJsonFlattenDecimal());
+ assertEquals(expected.getJsonFlattenNumber(), deserialized.getJsonFlattenNumber());
+ assertEquals(expected.getJsonFlattenString(), deserialized.getJsonFlattenString());
+ }
+
+ @Test
+ public void jsonFlattenWithJsonInfoDiscriminator() {
+ JsonFlattenWithJsonInfoDiscriminator expected = new JsonFlattenWithJsonInfoDiscriminator()
+ .setJsonFlattenDiscriminator("discriminator");
+
+ String expectedSerialization = "{\"type\":\"JsonFlattenWithJsonInfoDiscriminator\",\"jsonflatten\":{\"discriminator\":\"discriminator\"}}";
+ String actualSerialization = serialize(expected);
+
+ assertEquals(expectedSerialization, actualSerialization);
+
+ JsonFlattenWithJsonInfoDiscriminator deserialized = deserialize(actualSerialization,
+ JsonFlattenWithJsonInfoDiscriminator.class);
+ assertEquals(expected.getJsonFlattenDiscriminator(), deserialized.getJsonFlattenDiscriminator());
+ }
+
+ @Test
+ public void flattenedPropertiesAndJsonAnyGetter() {
+ FlattenedPropertiesAndJsonAnyGetter expected = new FlattenedPropertiesAndJsonAnyGetter()
+ .setString("string")
+ .addAdditionalProperty("key1", "value1")
+ .addAdditionalProperty("key2", "value2");
+
+ String expectedSerialization = "{\"flattened\":{\"string\":\"string\"},\"key1\":\"value1\",\"key2\":\"value2\"}";
+ String actualSerialization = serialize(expected);
+
+ assertEquals(expectedSerialization, actualSerialization);
+
+ FlattenedPropertiesAndJsonAnyGetter deserialized = deserialize(actualSerialization,
+ FlattenedPropertiesAndJsonAnyGetter.class);
+ assertEquals(expected.getString(), deserialized.getString());
+ assertEquals(expected.additionalProperties().size(), deserialized.additionalProperties().size());
+ for (String key : expected.additionalProperties().keySet()) {
+ assertEquals(expected.additionalProperties().get(key), deserialized.additionalProperties().get(key));
+ }
+ }
+
+ @Test
+ public void jsonFlattenFinalMap() {
+ final HashMap mapProperties = new HashMap() {{
+ put("/subscriptions/0-0-0-0-0/resourcegroups/0/providers/Microsoft.ManagedIdentity/0", "value");
+ }};
+ School school = new School().setTags(mapProperties);
+
+ String actualSerialization = serialize(school);
+ String expectedSerialization = "{\"tags\":{\"/subscriptions/0-0-0-0-0/resourcegroups"
+ + "/0/providers/Microsoft.ManagedIdentity/0\":\"value\"}}";
+ Assertions.assertEquals(expectedSerialization, actualSerialization);
+ }
+
+ @Test
+ public void jsonFlattenNestedInner() {
+ JsonFlattenNestedInner expected = new JsonFlattenNestedInner();
+ VirtualMachineIdentity identity = new VirtualMachineIdentity();
+ final Map map = new HashMap<>();
+ map.put("/subscriptions/0-0-0-0-0/resourcegroups/0/providers/Microsoft.ManagedIdentity/userAssignedIdentities/0",
+ new Object());
+ identity.setType(Arrays.asList("SystemAssigned, UserAssigned"));
+ identity.setUserAssignedIdentities(map);
+ expected.setIdentity(identity);
+
+ String expectedSerialization = "{\"identity\":{\"type\":[\"SystemAssigned, UserAssigned\"],"
+ + "\"userAssignedIdentities\":{\"/subscriptions/0-0-0-0-0/resourcegroups/0/providers/"
+ + "Microsoft.ManagedIdentity/userAssignedIdentities/0\":{}}}}";
+ String actualSerialization = serialize(expected);
+
+ Assertions.assertEquals(expectedSerialization, actualSerialization);
+ }
+
+ @Test
+ public void jsonFlattenRepeatedPropertyNameDeserialize() throws IOException {
+ SampleResource deserialized = JacksonAdapter.createDefaultSerializerAdapter().deserialize(
+ "{\"name\":\"...-01\",\"properties\":{\"registrationTtl\":\"10675199.02:48:05.4775807\",\"authorizationRules\":[]}}",
+ SampleResource.class,
+ SerializerEncoding.JSON
+ );
+
+ assertEquals("10675199.02:48:05.4775807", deserialized.getRegistrationTtl());
+ assertNull(deserialized.getNamePropertiesName());
+ }
+
+ @ParameterizedTest
+ @MethodSource("emptyDanglingNodeJsonSupplier")
+ public void jsonFlattenEmptyDanglingNodesDeserialize(String json, Object expected) throws IOException {
+ // test to verify null dangling nodes are still retained and set to null
+ FlattenDangling deserialized = JacksonAdapter.createDefaultSerializerAdapter().deserialize(
+ json,
+ FlattenDangling.class,
+ SerializerEncoding.JSON
+ );
+
+ assertEquals(expected, deserialized.getFlattenedProperty());
+ }
+
+ private static String serialize(Object object) {
+ try {
+ return ADAPTER.serialize(object, SerializerEncoding.JSON);
+ } catch (IOException ex) {
+ throw new UncheckedIOException(ex);
+ }
+ }
+
+ private static T deserialize(String json, Type type) {
+ try {
+ return ADAPTER.deserialize(json, type, SerializerEncoding.JSON);
+ } catch (IOException ex) {
+ throw new UncheckedIOException(ex);
+ }
+ }
+
+ private static Stream emptyDanglingNodeJsonSupplier() {
+ return Stream.of(
+ Arguments.of("{\"a\":{}}", null),
+
+ Arguments.of("{\"a\":{\"flattened\": {}}}", null),
+
+ Arguments.of("{\"a\":{\"flattened\": {\"property\": null}}}", null),
+
+ Arguments.of("{\"a\":{\"flattened\": {\"property\": \"value\"}}}", "value")
+ );
+ }
+}
+
diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/GeoSerializationTestHelpers.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/GeoSerializationTestHelpers.java
new file mode 100644
index 0000000000000..711240bc6ae29
--- /dev/null
+++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/GeoSerializationTestHelpers.java
@@ -0,0 +1,277 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.version.tests;
+
+import com.azure.core.implementation.GeoObjectHelper;
+import com.azure.core.models.GeoBoundingBox;
+import com.azure.core.models.GeoCollection;
+import com.azure.core.models.GeoLineString;
+import com.azure.core.models.GeoLineStringCollection;
+import com.azure.core.models.GeoLinearRing;
+import com.azure.core.models.GeoObject;
+import com.azure.core.models.GeoObjectType;
+import com.azure.core.models.GeoPoint;
+import com.azure.core.models.GeoPointCollection;
+import com.azure.core.models.GeoPolygon;
+import com.azure.core.models.GeoPolygonCollection;
+import com.azure.core.models.GeoPosition;
+import com.azure.core.util.CoreUtils;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Test helpers for {@code GeoJsonDeserializerTests} and {@code GeoJsonSerializerTests}.
+ */
+public class GeoSerializationTestHelpers {
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ static String geoToJson(GeoObject geoObject) {
+ if (geoObject instanceof GeoPoint) {
+ return pointToJson((GeoPoint) geoObject);
+ } else if (geoObject instanceof GeoLineString) {
+ return lineToJson((GeoLineString) geoObject);
+ } else if (geoObject instanceof GeoPolygon) {
+ return polygonToJson((GeoPolygon) geoObject);
+ } else if (geoObject instanceof GeoPointCollection) {
+ return multiPointToJson((GeoPointCollection) geoObject);
+ } else if (geoObject instanceof GeoLineStringCollection) {
+ return multiLineToJson((GeoLineStringCollection) geoObject);
+ } else if (geoObject instanceof GeoPolygonCollection) {
+ return multiPolygonToJson((GeoPolygonCollection) geoObject);
+ } else if (geoObject instanceof GeoCollection) {
+ return collectionToJson((GeoCollection) geoObject);
+ } else {
+ throw new IllegalStateException("Unknown geo type.");
+ }
+ }
+
+ private static String pointToJson(GeoPoint point) {
+ StringBuilder builder = new StringBuilder("{");
+ addType(GeoObjectType.POINT, builder);
+
+ builder.append(",\"coordinates\":");
+ addPosition(point.getCoordinates(), builder);
+
+ addAdditionalProperties(point, builder);
+
+ builder.append("}");
+
+ return builder.toString();
+ }
+
+ private static String lineToJson(GeoLineString line) {
+ StringBuilder builder = new StringBuilder("{");
+ addType(GeoObjectType.LINE_STRING, builder);
+
+ builder.append(",\"coordinates\":");
+ addLine(line.getCoordinates(), builder);
+
+ addAdditionalProperties(line, builder);
+
+ builder.append("}");
+
+ return builder.toString();
+ }
+
+ private static String polygonToJson(GeoPolygon polygon) {
+ StringBuilder builder = new StringBuilder("{");
+ addType(GeoObjectType.POLYGON, builder);
+
+ builder.append(",\"coordinates\":");
+ addPolygon(polygon.getRings(), builder);
+
+ addAdditionalProperties(polygon, builder);
+
+ builder.append("}");
+
+ return builder.toString();
+ }
+
+ private static String multiPointToJson(GeoPointCollection multiPoint) {
+ StringBuilder builder = new StringBuilder("{");
+ addType(GeoObjectType.MULTI_POINT, builder);
+
+ builder.append(",\"coordinates\":");
+ addLine(multiPoint.getPoints().stream().map(GeoPoint::getCoordinates).collect(Collectors.toList()), builder);
+
+ addAdditionalProperties(multiPoint, builder);
+
+ builder.append("}");
+
+ return builder.toString();
+
+ }
+
+ private static String multiLineToJson(GeoLineStringCollection multiLine) {
+ StringBuilder builder = new StringBuilder("{");
+ addType(GeoObjectType.MULTI_LINE_STRING, builder);
+
+ builder.append(",\"coordinates\":[");
+ for (int i = 0; i < multiLine.getLines().size(); i++) {
+ if (i > 0) {
+ builder.append(",");
+ }
+
+ addLine(multiLine.getLines().get(i).getCoordinates(), builder);
+ }
+
+ builder.append("]");
+
+ addAdditionalProperties(multiLine, builder);
+
+ builder.append("}");
+
+ return builder.toString();
+ }
+
+ private static String multiPolygonToJson(GeoPolygonCollection multiPolygon) {
+ StringBuilder builder = new StringBuilder("{");
+ addType(GeoObjectType.MULTI_POLYGON, builder);
+
+ builder.append(",\"coordinates\":[");
+
+ for (int i = 0; i < multiPolygon.getPolygons().size(); i++) {
+ if (i > 0) {
+ builder.append(",");
+ }
+
+ addPolygon(multiPolygon.getPolygons().get(i).getRings(), builder);
+ }
+
+ builder.append("]");
+
+ addAdditionalProperties(multiPolygon, builder);
+
+ builder.append("}");
+
+ return builder.toString();
+ }
+
+ private static String collectionToJson(GeoCollection collection) {
+ StringBuilder builder = new StringBuilder("{");
+ addType(GeoObjectType.GEOMETRY_COLLECTION, builder);
+
+ builder.append(",\"geometries\":[");
+
+ for (int i = 0; i < collection.getGeometries().size(); i++) {
+ if (i > 0) {
+ builder.append(",");
+ }
+
+ builder.append(geoToJson(collection.getGeometries().get(i)));
+ }
+
+ builder.append("]");
+
+ addAdditionalProperties(collection, builder);
+
+ builder.append("}");
+
+ return builder.toString();
+ }
+
+ private static void addType(GeoObjectType type, StringBuilder builder) {
+ builder.append("\"type\":\"").append(type.toString()).append("\"");
+ }
+
+ private static void addPosition(GeoPosition position, StringBuilder builder) {
+ builder.append("[")
+ .append(position.getLongitude())
+ .append(",")
+ .append(position.getLatitude());
+
+ Double altitude = position.getAltitude();
+ if (altitude != null) {
+ builder.append(",").append(altitude);
+ }
+
+ builder.append("]");
+ }
+
+ private static void addLine(List positions, StringBuilder builder) {
+ builder.append("[");
+
+ for (int i = 0; i < positions.size(); i++) {
+ if (i > 0) {
+ builder.append(",");
+ }
+
+ addPosition(positions.get(i), builder);
+ }
+
+ builder.append("]");
+ }
+
+ private static void addPolygon(List rings, StringBuilder builder) {
+ builder.append("[");
+
+ for (int i = 0; i < rings.size(); i++) {
+ if (i > 0) {
+ builder.append(",");
+ }
+
+ addLine(rings.get(i).getCoordinates(), builder);
+ }
+
+ builder.append("]");
+ }
+
+ private static void addAdditionalProperties(GeoObject geoObject, StringBuilder builder) {
+ addBoundingBox(geoObject.getBoundingBox(), builder);
+ addProperties(GeoObjectHelper.getCustomProperties(geoObject), builder);
+ }
+
+ private static void addBoundingBox(GeoBoundingBox boundingBox, StringBuilder builder) {
+ if (boundingBox == null) {
+ return;
+ }
+
+ builder.append(",\"bbox\":[")
+ .append(boundingBox.getWest())
+ .append(",")
+ .append(boundingBox.getSouth())
+ .append(",");
+
+ Double minAltitude = boundingBox.getMinAltitude();
+ if (minAltitude != null) {
+ builder.append(minAltitude).append(",");
+ }
+
+ builder.append(boundingBox.getEast())
+ .append(",")
+ .append(boundingBox.getNorth());
+
+ Double maxAltitude = boundingBox.getMaxAltitude();
+ if (maxAltitude != null) {
+ builder.append(",").append(maxAltitude);
+ }
+
+ builder.append("]");
+ }
+
+ private static void addProperties(Map properties, StringBuilder builder) {
+ if (CoreUtils.isNullOrEmpty(properties)) {
+ return;
+ }
+
+ try {
+ for (Map.Entry property : properties.entrySet()) {
+ if (builder.length() > 0) {
+ builder.append(",");
+ }
+
+ builder.append("\"")
+ .append(property.getKey())
+ .append("\":")
+ .append(MAPPER.writeValueAsString(property.getValue()));
+ }
+ } catch (JsonProcessingException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+}
diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/JacksonAdapterTests.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/JacksonAdapterTests.java
new file mode 100644
index 0000000000000..b8288e0f24ff9
--- /dev/null
+++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/JacksonAdapterTests.java
@@ -0,0 +1,407 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.version.tests;
+
+import com.azure.core.http.HttpHeaderName;
+import com.azure.core.http.HttpHeaders;
+import com.azure.core.http.HttpMethod;
+import com.azure.core.implementation.AccessibleByteArrayOutputStream;
+import com.azure.core.models.GeoObjectType;
+import com.azure.core.models.JsonPatchDocument;
+import com.azure.core.test.utils.TestUtils;
+import com.azure.core.util.DateTimeRfc1123;
+import com.azure.core.util.UrlBuilder;
+import com.azure.core.util.serializer.CollectionFormat;
+import com.azure.core.util.serializer.JacksonAdapter;
+import com.azure.core.util.serializer.SerializerEncoding;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDate;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoUnit;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class JacksonAdapterTests {
+ private static final JacksonAdapter ADAPTER = new JacksonAdapter();
+
+ @Test
+ public void emptyMap() throws IOException {
+ final Map map = new HashMap<>();
+ assertEquals("{}", ADAPTER.serialize(map, SerializerEncoding.JSON));
+ }
+
+ @Test
+ public void mapWithNullKey() throws IOException {
+ final Map map = new HashMap<>();
+ map.put(null, null);
+ assertEquals("{}", ADAPTER.serialize(map, SerializerEncoding.JSON));
+ }
+
+ @Test
+ public void mapWithEmptyKeyAndNullValue() throws IOException {
+ final MapHolder mapHolder = new MapHolder();
+ mapHolder.map(new HashMap<>());
+ mapHolder.map().put("", null);
+
+ assertEquals("{\"map\":{\"\":null}}", ADAPTER.serialize(mapHolder, SerializerEncoding.JSON));
+ }
+
+ @Test
+ public void mapWithEmptyKeyAndEmptyValue() throws IOException {
+ final MapHolder mapHolder = new MapHolder();
+ mapHolder.map = new HashMap<>();
+ mapHolder.map.put("", "");
+
+ assertEquals("{\"map\":{\"\":\"\"}}", ADAPTER.serialize(mapHolder, SerializerEncoding.JSON));
+ }
+
+ @Test
+ public void mapWithEmptyKeyAndNonEmptyValue() throws IOException {
+ final Map map = new HashMap<>();
+ map.put("", "test");
+
+ assertEquals("{\"\":\"test\"}", ADAPTER.serialize(map, SerializerEncoding.JSON));
+ }
+
+ private static class MapHolder {
+
+ @JsonInclude(content = JsonInclude.Include.ALWAYS)
+ private Map map = new HashMap<>();
+
+ public Map map() {
+ return map;
+ }
+
+ public void map(Map map) {
+ this.map = map;
+ }
+
+ }
+
+ @JacksonXmlRootElement(localName = "XmlString")
+ private static class XmlString {
+
+ @JsonProperty("Value")
+ private String value;
+
+ public String getValue() {
+ return value;
+ }
+
+ }
+
+ @ParameterizedTest
+ @MethodSource("serializeCollectionSupplier")
+ public void testSerializeList(List> values, CollectionFormat format, String expectedSerializedString) {
+ String actualSerializedString = JacksonAdapter.createDefaultSerializerAdapter()
+ .serializeList(values, format);
+ assertEquals(expectedSerializedString, actualSerializedString);
+ }
+
+ @ParameterizedTest
+ @MethodSource("serializeCollectionSupplier")
+ public void testSerializeIterable(Iterable> values, CollectionFormat format, String expectedSerializedString) {
+ String actualSerializedString = JacksonAdapter.createDefaultSerializerAdapter()
+ .serializeIterable(values, format);
+ assertEquals(expectedSerializedString, actualSerializedString);
+ }
+
+ @ParameterizedTest
+ @MethodSource("deserializeJsonSupplier")
+ public void deserializeJson(String json, OffsetDateTime expected) throws IOException {
+ DateTimeWrapper wrapper = JacksonAdapter.createDefaultSerializerAdapter()
+ .deserialize(json, DateTimeWrapper.class, SerializerEncoding.JSON);
+
+ assertEquals(expected, wrapper.getOffsetDateTime());
+ }
+
+ private static Stream serializeCollectionSupplier() {
+ return Stream.of(
+ Arguments.of(Arrays.asList("foo", "bar", "baz"), CollectionFormat.CSV, "foo,bar,baz"),
+ Arguments.of(Arrays.asList("foo", null, "baz"), CollectionFormat.CSV, "foo,,baz"),
+ Arguments.of(Arrays.asList(null, "bar", null, null), CollectionFormat.CSV, ",bar,,"),
+ Arguments.of(Arrays.asList(1, 2, 3), CollectionFormat.CSV, "1,2,3"),
+ Arguments.of(Arrays.asList(1, 2, 3), CollectionFormat.PIPES, "1|2|3"),
+ Arguments.of(Arrays.asList(1, 2, 3), CollectionFormat.SSV, "1 2 3"),
+ Arguments.of(Arrays.asList("foo", "bar", "baz"), CollectionFormat.MULTI, "foo&bar&baz")
+ );
+ }
+
+ private static Stream deserializeJsonSupplier() {
+ final String jsonFormat = "{\"OffsetDateTime\":\"%s\"}";
+ OffsetDateTime minValue = OffsetDateTime.of(1, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC);
+ OffsetDateTime unixEpoch = OffsetDateTime.of(1970, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC);
+
+ return Stream.of(
+ Arguments.of(String.format(jsonFormat, "0001-01-01T00:00:00"), minValue),
+ Arguments.of(String.format(jsonFormat, "0001-01-01T00:00:00Z"), minValue),
+ Arguments.of(String.format(jsonFormat, "1970-01-01T00:00:00"), unixEpoch),
+ Arguments.of(String.format(jsonFormat, "1970-01-01T00:00:00Z"), unixEpoch)
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("deserializeXmlSupplier")
+ public void deserializeXml(String xml, OffsetDateTime expected) throws IOException {
+ DateTimeWrapper wrapper = JacksonAdapter.createDefaultSerializerAdapter()
+ .deserialize(xml, DateTimeWrapper.class, SerializerEncoding.XML);
+
+ assertEquals(expected, wrapper.getOffsetDateTime());
+ }
+
+ private static Stream deserializeXmlSupplier() {
+ final String xmlFormat = "%s";
+ OffsetDateTime minValue = OffsetDateTime.of(1, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC);
+ OffsetDateTime unixEpoch = OffsetDateTime.of(1970, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC);
+
+ return Stream.of(
+ Arguments.of(String.format(xmlFormat, "0001-01-01T00:00:00"), minValue),
+ Arguments.of(String.format(xmlFormat, "0001-01-01T00:00:00Z"), minValue),
+ Arguments.of(String.format(xmlFormat, "1970-01-01T00:00:00"), unixEpoch),
+ Arguments.of(String.format(xmlFormat, "1970-01-01T00:00:00Z"), unixEpoch)
+ );
+ }
+
+ @JacksonXmlRootElement(localName = "Wrapper")
+ private static class DateTimeWrapper {
+ @JsonProperty(value = "OffsetDateTime", required = true)
+ private OffsetDateTime offsetDateTime;
+
+ public DateTimeWrapper setOffsetDateTime(OffsetDateTime offsetDateTime) {
+ this.offsetDateTime = offsetDateTime;
+ return this;
+ }
+
+ public OffsetDateTime getOffsetDateTime() {
+ return offsetDateTime;
+ }
+ }
+
+ @Test
+ public void stronglyTypedHeadersClassIsDeserialized() throws IOException {
+ final String expectedDate = DateTimeRfc1123.toRfc1123String(OffsetDateTime.now());
+
+ HttpHeaders rawHeaders = new HttpHeaders().set(HttpHeaderName.DATE, expectedDate);
+
+ StronglyTypedHeaders stronglyTypedHeaders = JacksonAdapter.createDefaultSerializerAdapter()
+ .deserialize(rawHeaders, StronglyTypedHeaders.class);
+
+ assertEquals(expectedDate, DateTimeRfc1123.toRfc1123String(stronglyTypedHeaders.getDate()));
+ }
+
+ @Test
+ public void stronglyTypedHeadersClassThrowsEagerly() {
+ HttpHeaders rawHeaders = new HttpHeaders().set(HttpHeaderName.DATE, "invalid-rfc1123-date");
+
+ assertThrows(DateTimeParseException.class, () -> JacksonAdapter.createDefaultSerializerAdapter()
+ .deserialize(rawHeaders, StronglyTypedHeaders.class));
+ }
+
+ @Test
+ public void invalidStronglyTypedHeadersClassThrowsCorrectException() throws IOException {
+ try {
+ JacksonAdapter.createDefaultSerializerAdapter().deserialize(new HttpHeaders(),
+ InvalidStronglyTypedHeaders.class);
+
+ fail("An exception should have been thrown.");
+ } catch (RuntimeException ex) {
+ assertTrue(ex.getCause() instanceof JsonProcessingException, "Exception cause type was "
+ + ex.getCause().getClass().getName() + " instead of the expected JsonProcessingException type.");
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("textSerializationSupplier")
+ public void textToStringSerialization(Object value, String expected) throws IOException {
+ assertEquals(expected, ADAPTER.serialize(value, SerializerEncoding.TEXT));
+ }
+
+ @ParameterizedTest
+ @MethodSource("textSerializationSupplier")
+ public void textToBytesSerialization(Object value, String expected) throws IOException {
+ byte[] actual = ADAPTER.serializeToBytes(value, SerializerEncoding.TEXT);
+
+ if (expected == null) {
+ assertNull(actual);
+ } else {
+ assertEquals(expected, new String(actual, StandardCharsets.UTF_8));
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("textSerializationSupplier")
+ public void textToOutputStreamSerialization(Object value, String expected) throws IOException {
+ AccessibleByteArrayOutputStream outputStream = new AccessibleByteArrayOutputStream();
+ ADAPTER.serialize(value, SerializerEncoding.TEXT, outputStream);
+
+ if (expected == null) {
+ assertEquals(0, outputStream.count());
+ } else {
+ assertEquals(expected, outputStream.toString(StandardCharsets.UTF_8));
+ }
+ }
+
+ private static Stream textSerializationSupplier() {
+ Map map = Collections.singletonMap("key", "value");
+
+ return Stream.of(
+ Arguments.of(1, "1"),
+ Arguments.of(1L, "1"),
+ Arguments.of(1.0F, "1.0"),
+ Arguments.of(1.0D, "1.0"),
+ Arguments.of("1", "1"),
+ Arguments.of(HttpMethod.GET, "GET"),
+ Arguments.of(GeoObjectType.POINT, "Point"),
+ Arguments.of(map, String.valueOf(map)),
+ Arguments.of(null, null)
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("textDeserializationSupplier")
+ public void stringToTextDeserialization(byte[] stringBytes, Class> type, Object expected) throws IOException {
+ Object actual = ADAPTER.deserialize(new String(stringBytes, StandardCharsets.UTF_8), type,
+ SerializerEncoding.TEXT);
+
+ if (type == byte[].class) {
+ TestUtils.assertArraysEqual((byte[]) expected, (byte[]) actual);
+ } else {
+ assertEquals(expected, actual);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("textDeserializationSupplier")
+ public void bytesToTextDeserialization(byte[] bytes, Class> type, Object expected) throws IOException {
+ Object actual = ADAPTER.deserialize(bytes, type, SerializerEncoding.TEXT);
+
+ if (type == byte[].class) {
+ TestUtils.assertArraysEqual((byte[]) expected, (byte[]) actual);
+ } else {
+ assertEquals(expected, actual);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("textDeserializationSupplier")
+ public void inputStreamToTextDeserialization(byte[] inputStreamBytes, Class> type, Object expected)
+ throws IOException {
+ Object actual = ADAPTER.deserialize(new ByteArrayInputStream(inputStreamBytes), type, SerializerEncoding.TEXT);
+
+ if (type == byte[].class) {
+ TestUtils.assertArraysEqual((byte[]) expected, (byte[]) actual);
+ } else {
+ assertEquals(expected, actual);
+ }
+ }
+
+ private static Stream textDeserializationSupplier() throws MalformedURLException {
+ byte[] helloBytes = "hello".getBytes(StandardCharsets.UTF_8);
+ String urlUri = "https://azure.com";
+ byte[] urlUriBytes = urlUri.getBytes(StandardCharsets.UTF_8);
+ OffsetDateTime offsetDateTime = OffsetDateTime.now(ZoneOffset.UTC).truncatedTo(ChronoUnit.SECONDS);
+ DateTimeRfc1123 dateTimeRfc1123 = new DateTimeRfc1123(offsetDateTime);
+ LocalDate localDate = LocalDate.now(ZoneOffset.UTC);
+ UUID uuid = UUID.randomUUID();
+ HttpMethod httpMethod = HttpMethod.GET;
+ GeoObjectType geoObjectType = GeoObjectType.POINT;
+
+ return Stream.of(
+ Arguments.of(helloBytes, String.class, "hello"),
+ Arguments.of(helloBytes, CharSequence.class, "hello"),
+ Arguments.of("1".getBytes(StandardCharsets.UTF_8), int.class, 1),
+ Arguments.of("1".getBytes(StandardCharsets.UTF_8), Integer.class, 1),
+ Arguments.of("1".getBytes(StandardCharsets.UTF_8), byte.class, (byte) 49),
+ Arguments.of("1".getBytes(StandardCharsets.UTF_8), Byte.class, (byte) 49),
+ Arguments.of("1".getBytes(StandardCharsets.UTF_8), long.class, 1L),
+ Arguments.of("1".getBytes(StandardCharsets.UTF_8), Long.class, 1L),
+ Arguments.of("1".getBytes(StandardCharsets.UTF_8), short.class, (short) 1),
+ Arguments.of("1".getBytes(StandardCharsets.UTF_8), Short.class, (short) 1),
+ Arguments.of("1.0".getBytes(StandardCharsets.UTF_8), double.class, 1.0D),
+ Arguments.of("1.0".getBytes(StandardCharsets.UTF_8), Double.class, 1.0D),
+ Arguments.of("1.0".getBytes(StandardCharsets.UTF_8), float.class, 1.0F),
+ Arguments.of("1.0".getBytes(StandardCharsets.UTF_8), Float.class, 1.0F),
+ Arguments.of("1".getBytes(StandardCharsets.UTF_8), char.class, '1'),
+ Arguments.of("1".getBytes(StandardCharsets.UTF_8), Character.class, '1'),
+ Arguments.of("1".getBytes(StandardCharsets.UTF_8), byte[].class, "1".getBytes(StandardCharsets.UTF_8)),
+ Arguments.of("true".getBytes(StandardCharsets.UTF_8), boolean.class, true),
+ Arguments.of("true".getBytes(StandardCharsets.UTF_8), Boolean.class, true),
+ Arguments.of(urlUriBytes, URL.class, UrlBuilder.parse(urlUri).toUrl()),
+ Arguments.of(urlUriBytes, URI.class, URI.create(urlUri)),
+ Arguments.of(getObjectBytes(offsetDateTime), OffsetDateTime.class, offsetDateTime),
+ Arguments.of(getObjectBytes(dateTimeRfc1123), DateTimeRfc1123.class, dateTimeRfc1123),
+ Arguments.of(getObjectBytes(localDate), LocalDate.class, localDate),
+ Arguments.of(getObjectBytes(uuid), UUID.class, uuid),
+ Arguments.of(getObjectBytes(httpMethod), HttpMethod.class, httpMethod),
+ Arguments.of(getObjectBytes(geoObjectType), GeoObjectType.class, geoObjectType)
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("textUnsupportedDeserializationSupplier")
+ public void unsupportedTextTypesDeserialization(Class> unsupportedType,
+ Class extends Throwable> exceptionType) {
+ assertThrows(exceptionType, () -> ADAPTER.deserialize(":////", unsupportedType, SerializerEncoding.TEXT));
+ }
+
+ private static Stream textUnsupportedDeserializationSupplier() {
+ return Stream.of(
+ Arguments.of(InputStream.class, IllegalStateException.class),
+ Arguments.of(JsonPatchDocument.class, IllegalStateException.class),
+ Arguments.of(URL.class, IOException.class), // Thrown when the String isn't a valid URL
+ Arguments.of(URI.class, IllegalArgumentException.class) // Thrown when the String isn't a valid URI
+ );
+ }
+
+ public static final class StronglyTypedHeaders {
+ private final DateTimeRfc1123 date;
+
+ public StronglyTypedHeaders(HttpHeaders rawHeaders) {
+ String dateString = rawHeaders.getValue(HttpHeaderName.DATE);
+ this.date = (dateString == null) ? null : new DateTimeRfc1123(dateString);
+ }
+
+ OffsetDateTime getDate() {
+ return (date == null) ? null : date.getDateTime();
+ }
+ }
+
+ public static final class InvalidStronglyTypedHeaders {
+ public InvalidStronglyTypedHeaders(HttpHeaders httpHeaders) throws Exception {
+ throw new Exception();
+ }
+ }
+
+ private static byte[] getObjectBytes(Object value) {
+ return String.valueOf(value).getBytes(StandardCharsets.UTF_8);
+ }
+}
diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/JsonSerializableEndToEndTests.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/JsonSerializableEndToEndTests.java
new file mode 100644
index 0000000000000..77397d247a8e1
--- /dev/null
+++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/JsonSerializableEndToEndTests.java
@@ -0,0 +1,163 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.version.tests;
+
+import com.azure.core.util.serializer.JacksonAdapter;
+import com.azure.core.util.serializer.SerializerAdapter;
+import com.azure.core.util.serializer.SerializerEncoding;
+import com.azure.json.JsonReader;
+import com.azure.json.JsonSerializable;
+import com.azure.json.JsonToken;
+import com.azure.json.JsonWriter;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.util.Objects;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Tests that the Jackson Databind {@link com.fasterxml.jackson.databind.Module} is able to handle deserializing and
+ * serializing {@link JsonSerializable} within an {@link ObjectMapper} context.
+ */
+public class JsonSerializableEndToEndTests {
+ private static final SerializerAdapter ADAPTER = JacksonAdapter.createDefaultSerializerAdapter();
+
+ @Test
+ public void serialization() throws IOException {
+ JsonSerializableWrapper wrapper = new JsonSerializableWrapper()
+ .setGeneralProperties(new GeneralProperties(42, true, "hello world", -0.0D));
+ String expected = "{\"jsonserializable\":{\"anInt\":42,\"aBoolean\":true,\"aString\":\"hello world\","
+ + "\"aNullableDecimal\":-0.0}}";
+
+ String actual = ADAPTER.serialize(wrapper, SerializerEncoding.JSON);
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void deserialization() throws IOException {
+ String json = "{\"jsonserializable\":{\"anInt\":42,\"aBoolean\":true,\"aString\":\"hello world\","
+ + "\"aNullableDecimal\":-0.0}}";
+ JsonSerializableWrapper expected = new JsonSerializableWrapper()
+ .setGeneralProperties(new GeneralProperties(42, true, "hello world", -0.0D));
+
+ JsonSerializableWrapper actual = ADAPTER.deserialize(json, JsonSerializableWrapper.class,
+ SerializerEncoding.JSON);
+ assertEquals(expected, actual);
+ }
+
+ /**
+ * Class that wraps a {@link JsonSerializable} type.
+ */
+ public static final class JsonSerializableWrapper {
+ @JsonProperty("jsonserializable")
+ private GeneralProperties generalProperties;
+
+ public JsonSerializableWrapper setGeneralProperties(GeneralProperties generalProperties) {
+ this.generalProperties = generalProperties;
+ return this;
+ }
+
+ public GeneralProperties getGeneralProperties() {
+ return generalProperties;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(generalProperties);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof JsonSerializableWrapper)) {
+ return false;
+ }
+
+ return Objects.equals(generalProperties, ((JsonSerializableWrapper) obj).generalProperties);
+ }
+ }
+
+ /**
+ * Class that implements {@link JsonSerializable}.
+ */
+ public static final class GeneralProperties implements JsonSerializable {
+ private final int anInt;
+ private final boolean aBoolean;
+ private final String aString;
+ private final Double aNullableDecimal;
+
+ public GeneralProperties(int anInt, boolean aBoolean, String aString, Double aNullableDecimal) {
+ this.anInt = anInt;
+ this.aBoolean = aBoolean;
+ this.aString = aString;
+ this.aNullableDecimal = aNullableDecimal;
+ }
+
+ @Override
+ public JsonWriter toJson(JsonWriter jsonWriter) throws IOException {
+ return jsonWriter.writeStartObject()
+ .writeIntField("anInt", anInt)
+ .writeBooleanField("aBoolean", aBoolean)
+ .writeStringField("aString", aString)
+ .writeNumberField("aNullableDecimal", aNullableDecimal)
+ .writeEndObject();
+ }
+
+ /**
+ * Deserializes an instance of {@link GeneralProperties} from the {@link JsonReader}.
+ *
+ * @param jsonReader The {@link JsonReader} being read.
+ * @return An instance of {@link GeneralProperties}, or null if the {@link JsonReader} was pointing to JSON
+ * null.
+ * @throws IOException If an error occurs during deserialization.
+ */
+ public static GeneralProperties fromJson(JsonReader jsonReader) throws IOException {
+ return jsonReader.readObject(reader -> {
+ int anInt = 0;
+ boolean aBoolean = false;
+ String aString = null;
+ Double aNullableDouble = null;
+
+ while (reader.nextToken() != JsonToken.END_OBJECT) {
+ String fieldName = reader.getFieldName();
+ reader.nextToken();
+
+ if ("anInt".equals(fieldName)) {
+ anInt = reader.getInt();
+ } else if ("aBoolean".equals(fieldName)) {
+ aBoolean = reader.getBoolean();
+ } else if ("aString".equals(fieldName)) {
+ aString = reader.getString();
+ } else if ("aNullableDecimal".equals(fieldName)) {
+ aNullableDouble = reader.getNullable(JsonReader::getDouble);
+ } else {
+ reader.skipChildren();
+ }
+ }
+
+ return new GeneralProperties(anInt, aBoolean, aString, aNullableDouble);
+ });
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(anInt, aBoolean, aString, aNullableDecimal);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof GeneralProperties)) {
+ return false;
+ }
+
+ GeneralProperties other = (GeneralProperties) obj;
+ return anInt == other.anInt
+ && aBoolean == other.aBoolean
+ && Objects.equals(aString, other.aString)
+ && Objects.equals(aNullableDecimal, other.aNullableDecimal);
+ }
+ }
+}
diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/ResponseErrorDeserializerTests.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/ResponseErrorDeserializerTests.java
new file mode 100644
index 0000000000000..cf5ff7c5dbf4a
--- /dev/null
+++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/ResponseErrorDeserializerTests.java
@@ -0,0 +1,62 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.version.tests;
+
+import com.azure.core.models.ResponseError;
+import com.azure.core.util.serializer.JacksonAdapter;
+import com.azure.core.util.serializer.SerializerEncoding;
+import com.fasterxml.jackson.databind.exc.MismatchedInputException;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.io.IOException;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Unit tests for {@code ResponseErrorDeserializer}.
+ */
+public class ResponseErrorDeserializerTests {
+ private static final JacksonAdapter MAPPER = new JacksonAdapter();
+
+ @ParameterizedTest
+ @MethodSource("deserializeOffsetDateTimeSupplier")
+ public void deserializeJson(String responseWithError, String expectedCode, String expectedMessage) throws IOException {
+ ResponseError deserialize = MAPPER.deserialize(responseWithError, ResponseError.class, SerializerEncoding.JSON);
+ assertEquals(expectedCode, deserialize.getCode());
+ assertEquals(expectedMessage, deserialize.getMessage());
+ }
+
+ @Test
+ public void deserializeResponseErrorMissingRequiredProperty() {
+ // code is a required property and exception should be thrown if it's missing
+ String missingCodeWithErrorWrapper = "{\"error\": {\"message\": \"Invalid syntax\"}}";
+ Assertions.assertThrows(MismatchedInputException.class, () -> MAPPER.deserialize(missingCodeWithErrorWrapper, ResponseError.class, SerializerEncoding.JSON));
+
+ String missingCodeWithoutErrorWrapper = "{\"message\": \"Invalid syntax\"}";
+ Assertions.assertThrows(MismatchedInputException.class, () -> MAPPER.deserialize(missingCodeWithoutErrorWrapper, ResponseError.class, SerializerEncoding.JSON));
+
+ // message is a required property and exception should be thrown if it's missing
+ String missingMessageWithErrorWrapper = "{\"error\": {\"code\": \"BAD_QUERY_FORMAT\"}}";
+ Assertions.assertThrows(MismatchedInputException.class, () -> MAPPER.deserialize(missingMessageWithErrorWrapper, ResponseError.class, SerializerEncoding.JSON));
+
+ String missingMessageWithoutErrorWrapper = "{\"code\": \"BAD_QUERY_FORMAT\"}";
+ Assertions.assertThrows(MismatchedInputException.class, () -> MAPPER.deserialize(missingMessageWithoutErrorWrapper, ResponseError.class, SerializerEncoding.JSON));
+ }
+
+ private static Stream deserializeOffsetDateTimeSupplier() {
+
+ return Stream.of(
+ Arguments.of("{\"error\": {\"code\": \"BAD_QUERY_FORMAT\", \"message\": \"Invalid syntax\"}}", "BAD_QUERY_FORMAT", "Invalid syntax"),
+ Arguments.of("{\"code\": \"BAD_QUERY_FORMAT\", \"message\": \"Invalid syntax\"}", "BAD_QUERY_FORMAT", "Invalid syntax"),
+ Arguments.of("{\"name\": \"foo\", \"error\": {\"code\": \"BAD_QUERY_FORMAT\", \"message\": \"Invalid syntax\"}}", "BAD_QUERY_FORMAT", "Invalid syntax"),
+ Arguments.of("{\"name\": \"foo\", \"code\": \"BAD_QUERY_FORMAT\", \"message\": \"Invalid syntax\"}", "BAD_QUERY_FORMAT", "Invalid syntax")
+ );
+ }
+
+}
diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/RestProxyTests.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/RestProxyTests.java
new file mode 100644
index 0000000000000..376f8c3851eae
--- /dev/null
+++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/RestProxyTests.java
@@ -0,0 +1,443 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.version.tests;
+
+import com.azure.core.annotation.BodyParam;
+import com.azure.core.annotation.ExpectedResponses;
+import com.azure.core.annotation.Get;
+import com.azure.core.annotation.HeaderParam;
+import com.azure.core.annotation.Host;
+import com.azure.core.annotation.PathParam;
+import com.azure.core.annotation.Post;
+import com.azure.core.annotation.ServiceInterface;
+import com.azure.core.http.HttpClient;
+import com.azure.core.http.HttpHeaderName;
+import com.azure.core.http.HttpMethod;
+import com.azure.core.http.HttpPipeline;
+import com.azure.core.http.HttpPipelineBuilder;
+import com.azure.core.http.HttpRequest;
+import com.azure.core.http.HttpResponse;
+import com.azure.core.http.rest.RequestOptions;
+import com.azure.core.http.rest.Response;
+import com.azure.core.http.rest.RestProxy;
+import com.azure.core.http.rest.StreamResponse;
+import com.azure.core.implementation.http.rest.RestProxyUtils;
+import com.azure.core.implementation.util.BinaryDataContent;
+import com.azure.core.implementation.util.BinaryDataHelper;
+import com.azure.core.test.http.MockHttpResponse;
+import com.azure.core.util.BinaryData;
+import com.azure.core.util.Context;
+import org.junit.jupiter.api.Named;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+import java.io.ByteArrayInputStream;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests {@link RestProxy}.
+ */
+public class RestProxyTests {
+
+ @Host("https://azure.com")
+ @ServiceInterface(name = "myService")
+ interface TestInterface {
+ @Post("my/url/path")
+ @ExpectedResponses({200})
+ Mono> testMethod(
+ @BodyParam("application/octet-stream") Flux request,
+ @HeaderParam("Content-Type") String contentType,
+ @HeaderParam("Content-Length") Long contentLength
+ );
+
+ @Post("my/url/path")
+ @ExpectedResponses({200})
+ Mono> testMethod(
+ @BodyParam("application/octet-stream") BinaryData data,
+ @HeaderParam("Content-Type") String contentType,
+ @HeaderParam("Content-Length") Long contentLength
+ );
+
+ @Get("{nextLink}")
+ @ExpectedResponses({200})
+ Mono> testListNext(@PathParam(value = "nextLink", encoded = true) String nextLink);
+
+ @Get("my/url/path")
+ @ExpectedResponses({200})
+ Mono testMethodReturnsMonoVoid();
+
+ @Get("my/url/path")
+ @ExpectedResponses({200})
+ void testVoidMethod();
+
+ @Get("my/url/path")
+ @ExpectedResponses({200})
+ Mono> testMethodReturnsMonoResponseVoid();
+
+ @Get("my/url/path")
+ @ExpectedResponses({200})
+ Response testMethodReturnsResponseVoid();
+
+ @Get("my/url/path")
+ @ExpectedResponses({200})
+ StreamResponse testDownload();
+
+ @Get("my/url/path")
+ @ExpectedResponses({200})
+ Mono testDownloadAsync();
+ }
+
+ @Test
+ public void contentTypeHeaderPriorityOverBodyParamAnnotationTest() {
+ HttpClient client = new LocalHttpClient();
+ HttpPipeline pipeline = new HttpPipelineBuilder()
+ .httpClient(client)
+ .build();
+
+ TestInterface testInterface = RestProxy.create(TestInterface.class, pipeline);
+ byte[] bytes = "hello".getBytes();
+ Response response = testInterface.testMethod(Flux.just(ByteBuffer.wrap(bytes)),
+ "application/json", (long) bytes.length)
+ .block();
+ assertEquals(200, response.getStatusCode());
+ }
+
+ @Test
+ public void streamResponseShouldHaveHttpResponseReferenceSync() {
+ LocalHttpClient client = new LocalHttpClient();
+ HttpPipeline pipeline = new HttpPipelineBuilder()
+ .httpClient(client)
+ .build();
+
+ TestInterface testInterface = RestProxy.create(TestInterface.class, pipeline);
+ StreamResponse streamResponse = testInterface.testDownload();
+ streamResponse.close();
+ // This indirectly tests that StreamResponse has HttpResponse reference
+ assertTrue(client.closeCalledOnResponse);
+ }
+
+ @Test
+ public void streamResponseShouldHaveHttpResponseReferenceAsync() {
+ LocalHttpClient client = new LocalHttpClient();
+ HttpPipeline pipeline = new HttpPipelineBuilder()
+ .httpClient(client)
+ .build();
+
+ TestInterface testInterface = RestProxy.create(TestInterface.class, pipeline);
+ StepVerifier.create(
+ testInterface.testDownloadAsync()
+ .doOnNext(StreamResponse::close))
+ .expectNextCount(1)
+ .verifyComplete();
+ // This indirectly tests that StreamResponse has HttpResponse reference
+ assertTrue(client.closeCalledOnResponse);
+ }
+
+ @ParameterizedTest
+ @MethodSource("knownLengthBinaryDataIsPassthroughArgumentProvider")
+ public void knownLengthBinaryDataIsPassthrough(BinaryData data, long contentLength) {
+ LocalHttpClient client = new LocalHttpClient();
+ HttpPipeline pipeline = new HttpPipelineBuilder()
+ .httpClient(client)
+ .build();
+
+ TestInterface testInterface = RestProxy.create(TestInterface.class, pipeline);
+ Response response = testInterface.testMethod(data, "application/json", contentLength).block();
+ assertEquals(200, response.getStatusCode());
+ assertSame(data, client.getLastHttpRequest().getBodyAsBinaryData());
+ }
+
+ private static Stream knownLengthBinaryDataIsPassthroughArgumentProvider() throws Exception {
+ String string = "hello";
+ byte[] bytes = string.getBytes();
+ Path file = Files.createTempFile("knownLengthBinaryDataIsPassthroughArgumentProvider", null);
+ file.toFile().deleteOnExit();
+ Files.write(file, bytes);
+ return Stream.of(
+ Arguments.of(Named.of("bytes", BinaryData.fromBytes(bytes)), bytes.length),
+ Arguments.of(Named.of("string", BinaryData.fromString(string)), bytes.length),
+ Arguments.of(Named.of("file", BinaryData.fromFile(file)), bytes.length),
+ Arguments.of(Named.of("serializable", BinaryData.fromObject(bytes)),
+ BinaryData.fromObject(bytes).getLength())
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("doesNotChangeBinaryDataContentTypeDataProvider")
+ public void doesNotChangeBinaryDataContentType(BinaryData data, long contentLength) {
+ LocalHttpClient client = new LocalHttpClient();
+ HttpPipeline pipeline = new HttpPipelineBuilder()
+ .httpClient(client)
+ .build();
+ Class extends BinaryDataContent> expectedContentClazz = BinaryDataHelper.getContent(data).getClass();
+
+
+ TestInterface testInterface = RestProxy.create(TestInterface.class, pipeline);
+ Response response = testInterface.testMethod(data,
+ "application/json", contentLength)
+ .block();
+ assertEquals(200, response.getStatusCode());
+
+ Class extends BinaryDataContent> actualContentClazz = BinaryDataHelper.getContent(
+ client.getLastHttpRequest().getBodyAsBinaryData()).getClass();
+ assertEquals(expectedContentClazz, actualContentClazz);
+ }
+
+ @Test
+ public void monoVoidReturningApiClosesResponse() {
+ LocalHttpClient client = new LocalHttpClient();
+ HttpPipeline pipeline = new HttpPipelineBuilder()
+ .httpClient(client)
+ .build();
+
+ TestInterface testInterface = RestProxy.create(TestInterface.class, pipeline);
+ StepVerifier.create(testInterface.testMethodReturnsMonoVoid())
+ .verifyComplete();
+
+ assertTrue(client.closeCalledOnResponse);
+ }
+
+ @Test
+ public void voidReturningApiClosesResponse() {
+ LocalHttpClient client = new LocalHttpClient();
+ HttpPipeline pipeline = new HttpPipelineBuilder()
+ .httpClient(client)
+ .build();
+
+ TestInterface testInterface = RestProxy.create(TestInterface.class, pipeline);
+
+ testInterface.testVoidMethod();
+
+ assertTrue(client.closeCalledOnResponse);
+ }
+
+ @Test
+ public void voidReturningApiIgnoresResponseBody() {
+ LocalHttpClient client = new LocalHttpClient();
+ HttpPipeline pipeline = new HttpPipelineBuilder()
+ .httpClient(client)
+ .build();
+
+ TestInterface testInterface = RestProxy.create(TestInterface.class, pipeline);
+
+ testInterface.testVoidMethod();
+
+ assertFalse(client.lastContext.getData("azure-eagerly-read-response").isPresent());
+ assertTrue(client.lastContext.getData("azure-ignore-response-body").isPresent());
+ assertTrue((boolean) client.lastContext.getData("azure-ignore-response-body").get());
+ }
+
+ @Test
+ public void monoVoidReturningApiIgnoresResponseBody() {
+ LocalHttpClient client = new LocalHttpClient();
+ HttpPipeline pipeline = new HttpPipelineBuilder()
+ .httpClient(client)
+ .build();
+
+ TestInterface testInterface = RestProxy.create(TestInterface.class, pipeline);
+ StepVerifier.create(
+ testInterface.testMethodReturnsMonoVoid())
+ .verifyComplete();
+
+ assertFalse(client.lastContext.getData("azure-eagerly-read-response").isPresent());
+ assertTrue(client.lastContext.getData("azure-ignore-response-body").isPresent());
+ assertTrue((boolean) client.lastContext.getData("azure-ignore-response-body").get());
+ }
+
+ @Test
+ public void monoResponseVoidReturningApiIgnoresResponseBody() {
+ LocalHttpClient client = new LocalHttpClient();
+ HttpPipeline pipeline = new HttpPipelineBuilder()
+ .httpClient(client)
+ .build();
+
+ TestInterface testInterface = RestProxy.create(TestInterface.class, pipeline);
+ StepVerifier.create(
+ testInterface.testMethodReturnsMonoResponseVoid())
+ .expectNextCount(1)
+ .verifyComplete();
+
+ assertFalse(client.lastContext.getData("azure-eagerly-read-response").isPresent());
+ assertTrue(client.lastContext.getData("azure-ignore-response-body").isPresent());
+ assertTrue((boolean) client.lastContext.getData("azure-ignore-response-body").get());
+ }
+
+ @Test
+ public void responseVoidReturningApiIgnoresResponseBody() {
+ LocalHttpClient client = new LocalHttpClient();
+ HttpPipeline pipeline = new HttpPipelineBuilder()
+ .httpClient(client)
+ .build();
+
+
+ TestInterface testInterface = RestProxy.create(TestInterface.class, pipeline);
+ testInterface.testMethodReturnsResponseVoid();
+
+ assertFalse(client.lastContext.getData("azure-eagerly-read-response").isPresent());
+ assertTrue(client.lastContext.getData("azure-ignore-response-body").isPresent());
+ assertTrue((boolean) client.lastContext.getData("azure-ignore-response-body").get());
+ }
+
+ @Test
+ public void streamResponseDoesNotEagerlyReadsResponse() {
+ LocalHttpClient client = new LocalHttpClient();
+ HttpPipeline pipeline = new HttpPipelineBuilder()
+ .httpClient(client)
+ .build();
+
+
+ TestInterface testInterface = RestProxy.create(TestInterface.class, pipeline);
+ testInterface.testDownload();
+
+ assertFalse(client.lastContext.getData("azure-eagerly-read-response").isPresent());
+ }
+
+ @Test
+ public void monoWithStreamResponseDoesNotEagerlyReadsResponse() {
+ LocalHttpClient client = new LocalHttpClient();
+ HttpPipeline pipeline = new HttpPipelineBuilder()
+ .httpClient(client)
+ .build();
+
+
+ TestInterface testInterface = RestProxy.create(TestInterface.class, pipeline);
+ StepVerifier.create(testInterface.testDownloadAsync().doOnNext(StreamResponse::close))
+ .expectNextCount(1)
+ .verifyComplete();
+
+ assertFalse(client.lastContext.getData("azure-eagerly-read-response").isPresent());
+ }
+
+ private static Stream doesNotChangeBinaryDataContentTypeDataProvider() throws Exception {
+ String string = "hello";
+ byte[] bytes = string.getBytes();
+ Path file = Files.createTempFile("doesNotChangeBinaryDataContentTypeDataProvider", null);
+ file.toFile().deleteOnExit();
+ Files.write(file, bytes);
+ ByteArrayInputStream stream = new ByteArrayInputStream(bytes);
+ return Stream.of(
+ Arguments.of(Named.of("bytes", BinaryData.fromBytes(bytes)), bytes.length),
+ Arguments.of(Named.of("string", BinaryData.fromString(string)), bytes.length),
+ Arguments.of(Named.of("file", BinaryData.fromFile(file)), bytes.length),
+ Arguments.of(Named.of("stream", BinaryData.fromStream(stream, (long) bytes.length)), bytes.length),
+ Arguments.of(Named.of("eager flux with length",
+ BinaryData.fromFlux(Flux.just(ByteBuffer.wrap(bytes))).block()), bytes.length),
+ Arguments.of(Named.of("lazy flux",
+ BinaryData.fromFlux(Flux.just(ByteBuffer.wrap(bytes)), null, false).block()), bytes.length),
+ Arguments.of(Named.of("lazy flux with length",
+ BinaryData.fromFlux(Flux.just(ByteBuffer.wrap(bytes)), (long) bytes.length, false).block()), bytes.length),
+ Arguments.of(Named.of("serializable", BinaryData.fromObject(bytes)),
+ BinaryData.fromObject(bytes).getLength())
+ );
+ }
+
+ private static final class LocalHttpClient implements HttpClient {
+
+ private volatile HttpRequest lastHttpRequest;
+ private volatile Context lastContext;
+ private volatile boolean closeCalledOnResponse;
+
+ @Override
+ public Mono send(HttpRequest request) {
+ return send(request, Context.NONE);
+ }
+
+ @Override
+ public Mono send(HttpRequest request, Context context) {
+ lastHttpRequest = request;
+ lastContext = context;
+ boolean success = request.getUrl().getPath().equals("/my/url/path");
+ if (request.getHttpMethod().equals(HttpMethod.POST)) {
+ success &= "application/json".equals(request.getHeaders().getValue(HttpHeaderName.CONTENT_TYPE));
+ } else {
+ success &= request.getHttpMethod().equals(HttpMethod.GET);
+ }
+
+ return Mono.just(new MockHttpResponse(request, success ? 200 : 400) {
+ @Override
+ public void close() {
+ closeCalledOnResponse = true;
+ super.close();
+ }
+ });
+ }
+
+ public HttpRequest getLastHttpRequest() {
+ return lastHttpRequest;
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("mergeRequestOptionsContextSupplier")
+ public void mergeRequestOptionsContext(Context context, RequestOptions options,
+ Map