Repository: eclipse-vertx/vert.x

Problem statement:
Implementation of JSON RFCs introduced inconsistency to Json{Object|Array} API
### Version

Vert.x 4.0.0

### Context

The implementation of the JSON RFCs (#3197) removed the type checking from methods JsonObject.put(String, Object) and JsonArray.add(int, Object) in order to allow future POJO mappings. Nevertheless the methods Json{Object|Array}.copy() are still checking types. This leads to the situation that developers are able to successfully create Json{Object|Array}s that cannot be copied afterwards.
When using the following piece of code for creating valid JsonObjects
```
  try {
    jsonObject.put(key, value);
  }
  catch (IllegalStateExcveption e) {
    jsonObject.put(key, value.toString());
  }
```
which are copied afterwards, #3197 breaks that piece of code and is not backward compatible.

### Do you have a reproducer?

A small reproducer can be found at https://github.com/riemenschneider/JDBCPreparedQueryTest. The test fails with Vert.x 4.0.0 and runs successful using Vert.x 3.9.5.

### Steps to reproduce

1. Clone the reproducer mentioned above.
2. Run ``gradlew test``.
3. Change Vert.x dependency in ``build.gradle`` to ``io.vertx:vertx-core:3.9.5`` and run again ``gradlew test``.



Hints:
do you mean https://github.com/riemenschneider/JSONObjectTest as reproducer ?

Patch:
diff --git a/src/main/java/io/vertx/core/json/JsonArray.java b/src/main/java/io/vertx/core/json/JsonArray.java
--- a/src/main/java/io/vertx/core/json/JsonArray.java
+++ b/src/main/java/io/vertx/core/json/JsonArray.java
@@ -12,11 +12,13 @@
 package io.vertx.core.json;
 
 import io.vertx.core.buffer.Buffer;
+import io.vertx.core.json.impl.JsonUtil;
 import io.vertx.core.shareddata.Shareable;
 import io.vertx.core.shareddata.impl.ClusterSerializable;
 
 import java.time.Instant;
 import java.util.*;
+import java.util.function.Function;
 import java.util.stream.Stream;
 
 import static io.vertx.core.json.impl.JsonUtil.*;
@@ -557,12 +559,26 @@ public String encodePrettily() {
    * Deep copy of the JSON array.
    *
    * @return a copy where all elements have been copied recursively
+   * @throws IllegalStateException when a nested element cannot be copied
    */
   @Override
   public JsonArray copy() {
+    return copy(DEFAULT_CLONER);
+  }
+
+  /**
+   * Deep copy of the JSON array.
+   *
+   * <p> Unlike {@link #copy()} that can fail when an unknown element cannot be copied, this method
+   * delegates the copy of such element to the {@code cloner} function and will not fail.
+   *
+   * @param cloner a function that copies custom values not supported by the JSON implementation
+   * @return a copy where all elements have been copied recursively
+   */
+  public JsonArray copy(Function<Object, ?> cloner) {
     List<Object> copiedList = new ArrayList<>(list.size());
     for (Object val : list) {
-      copiedList.add(checkAndCopy(val));
+      copiedList.add(deepCopy(val, cloner));
     }
     return new JsonArray(copiedList);
   }
diff --git a/src/main/java/io/vertx/core/json/JsonObject.java b/src/main/java/io/vertx/core/json/JsonObject.java
--- a/src/main/java/io/vertx/core/json/JsonObject.java
+++ b/src/main/java/io/vertx/core/json/JsonObject.java
@@ -11,11 +11,13 @@
 package io.vertx.core.json;
 
 import io.vertx.core.buffer.Buffer;
+import io.vertx.core.json.impl.JsonUtil;
 import io.vertx.core.shareddata.Shareable;
 import io.vertx.core.shareddata.impl.ClusterSerializable;
 
 import java.time.Instant;
 import java.util.*;
+import java.util.function.Function;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
 
@@ -751,9 +753,23 @@ public Buffer toBuffer() {
    * Deep copy of this JSON object.
    *
    * @return a copy where all elements have been copied recursively
+   * @throws IllegalStateException when a nested element cannot be copied
    */
   @Override
   public JsonObject copy() {
+    return copy(DEFAULT_CLONER);
+  }
+
+  /**
+   * Deep copy of this JSON object.
+   *
+   * <p> Unlike {@link #copy()} that can fail when an unknown element cannot be copied, this method
+   * delegates the copy of such element to the {@code cloner} function and will not fail.
+   *
+   * @param cloner a function that copies custom values not supported by the JSON implementation
+   * @return a copy where all elements have been copied recursively
+   */
+  public JsonObject copy(Function<Object, ?> cloner) {
     Map<String, Object> copiedMap;
     if (map instanceof LinkedHashMap) {
       copiedMap = new LinkedHashMap<>(map.size());
@@ -761,7 +777,7 @@ public JsonObject copy() {
       copiedMap = new HashMap<>(map.size());
     }
     for (Map.Entry<String, Object> entry : map.entrySet()) {
-      Object val = checkAndCopy(entry.getValue());
+      Object val = deepCopy(entry.getValue(), cloner);
       copiedMap.put(entry.getKey(), val);
     }
     return new JsonObject(copiedMap);
diff --git a/src/main/java/io/vertx/core/json/impl/JsonUtil.java b/src/main/java/io/vertx/core/json/impl/JsonUtil.java
--- a/src/main/java/io/vertx/core/json/impl/JsonUtil.java
+++ b/src/main/java/io/vertx/core/json/impl/JsonUtil.java
@@ -19,6 +19,7 @@
 import java.util.Base64;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Function;
 
 import static java.time.format.DateTimeFormatter.ISO_INSTANT;
 
@@ -81,8 +82,12 @@ public static Object wrapJsonValue(Object val) {
     return val;
   }
 
+  public static final Function<Object, ?> DEFAULT_CLONER = o -> {
+    throw new IllegalStateException("Illegal type in Json: " + o.getClass());
+  };
+
   @SuppressWarnings("unchecked")
-  public static Object checkAndCopy(Object val) {
+  public static Object deepCopy(Object val, Function<Object, ?> copier) {
     if (val == null) {
       // OK
     } else if (val instanceof Number) {
@@ -101,9 +106,9 @@ public static Object checkAndCopy(Object val) {
       // JsonObject, JsonArray or any user defined type that can shared across the cluster
       val = ((Shareable) val).copy();
     } else if (val instanceof Map) {
-      val = (new JsonObject((Map) val)).copy();
+      val = (new JsonObject((Map) val)).copy(copier);
     } else if (val instanceof List) {
-      val = (new JsonArray((List) val)).copy();
+      val = (new JsonArray((List) val)).copy(copier);
     } else if (val instanceof Buffer) {
       val = ((Buffer) val).copy();
     } else if (val instanceof byte[]) {
@@ -113,7 +118,7 @@ public static Object checkAndCopy(Object val) {
     } else if (val instanceof Enum) {
       // OK
     } else {
-      throw new IllegalStateException("Illegal type in Json: " + val.getClass());
+      val = copier.apply(val);
     }
     return val;
   }


Test patch:
diff --git a/src/test/java/io/vertx/core/json/JsonArrayTest.java b/src/test/java/io/vertx/core/json/JsonArrayTest.java
--- a/src/test/java/io/vertx/core/json/JsonArrayTest.java
+++ b/src/test/java/io/vertx/core/json/JsonArrayTest.java
@@ -21,6 +21,7 @@
 import java.math.BigInteger;
 import java.time.Instant;
 import java.util.*;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
 import static io.vertx.core.json.impl.JsonUtil.BASE64_DECODER;
@@ -797,49 +798,55 @@ public void testCopy() {
   }
 
   @Test
-  public void testInvalidValsOnCopy() {
-    List<Object> invalid = new ArrayList<>();
-    invalid.add(new SomeClass());
-    JsonArray arr = new JsonArray(invalid);
+  public void testInvalidValsOnCopy1() {
+    SomeClass invalid = new SomeClass();
+    JsonArray array = new JsonArray(Collections.singletonList(invalid));
     try {
-      arr.copy();
+      array.copy();
       fail();
     } catch (IllegalStateException e) {
       // OK
     }
+    array = array.copy(SomeClass.CLONER);
+    assertTrue(array.getValue(0) instanceof SomeClass);
+    assertNotSame(array.getValue(0), invalid);
   }
 
   @Test
   public void testInvalidValsOnCopy2() {
-    List<Object> invalid = new ArrayList<>();
-    List<Object> invalid2 = new ArrayList<>();
-    invalid2.add(new SomeClass());
-    invalid.add(invalid2);
-    JsonArray arr = new JsonArray(invalid);
+    SomeClass invalid = new SomeClass();
+    JsonArray array = new JsonArray(Collections.singletonList(Collections.singletonMap("foo", invalid)));
     try {
-      arr.copy();
+      array.copy();
       fail();
     } catch (IllegalStateException e) {
       // OK
     }
+    array = array.copy(SomeClass.CLONER);
+    assertTrue(array.getJsonObject(0).getValue("foo") instanceof SomeClass);
+    assertNotSame(array.getJsonObject(0).getValue("foo"), invalid);
   }
 
   @Test
   public void testInvalidValsOnCopy3() {
-    List<Object> invalid = new ArrayList<>();
-    Map<String, Object> invalid2 = new HashMap<>();
-    invalid2.put("foo", new SomeClass());
-    invalid.add(invalid2);
-    JsonArray arr = new JsonArray(invalid);
+    SomeClass invalid = new SomeClass();
+    JsonArray array = new JsonArray(Collections.singletonList(Collections.singletonList(invalid)));
     try {
-      arr.copy();
+      array.copy();
       fail();
     } catch (IllegalStateException e) {
       // OK
     }
+    array = array.copy(SomeClass.CLONER);
+    assertTrue(array.getJsonArray(0).getValue(0) instanceof SomeClass);
+    assertNotSame(array.getJsonArray(0).getValue(0), invalid);
   }
 
-  class SomeClass {
+  static class SomeClass {
+    static final Function<Object, ?> CLONER = o -> {
+      assertTrue(o instanceof SomeClass);
+      return new SomeClass();
+    };
   }
 
   @Test
diff --git a/src/test/java/io/vertx/core/json/JsonObjectTest.java b/src/test/java/io/vertx/core/json/JsonObjectTest.java
--- a/src/test/java/io/vertx/core/json/JsonObjectTest.java
+++ b/src/test/java/io/vertx/core/json/JsonObjectTest.java
@@ -23,6 +23,7 @@
 import java.time.temporal.ChronoUnit;
 import java.util.*;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
 import static io.vertx.core.json.impl.JsonUtil.BASE64_DECODER;
@@ -1392,48 +1393,54 @@ public void testCopy() {
 
   @Test
   public void testInvalidValsOnCopy1() {
-    Map<String, Object> invalid = new HashMap<>();
-    invalid.put("foo", new SomeClass());
-    JsonObject object = new JsonObject(invalid);
+    SomeClass invalid = new SomeClass();
+    JsonObject object = new JsonObject(Collections.singletonMap("foo", invalid));
     try {
       object.copy();
       fail();
     } catch (IllegalStateException e) {
       // OK
     }
+    object = object.copy(SomeClass.CLONER);
+    assertTrue(object.getValue("foo") instanceof SomeClass);
+    assertNotSame(object.getValue("foo"), invalid);
   }
 
   @Test
   public void testInvalidValsOnCopy2() {
-    Map<String, Object> invalid = new HashMap<>();
-    Map<String, Object> invalid2 = new HashMap<>();
-    invalid2.put("foo", new SomeClass());
-    invalid.put("bar",invalid2);
-    JsonObject object = new JsonObject(invalid);
+    SomeClass invalid = new SomeClass();
+    JsonObject object = new JsonObject(Collections.singletonMap("bar", Collections.singletonMap("foo", invalid)));
     try {
       object.copy();
       fail();
     } catch (IllegalStateException e) {
       // OK
     }
+    object = object.copy(SomeClass.CLONER);
+    assertTrue(object.getJsonObject("bar").getValue("foo") instanceof SomeClass);
+    assertNotSame(object.getJsonObject("bar").getValue("foo"), invalid);
   }
 
   @Test
   public void testInvalidValsOnCopy3() {
-    Map<String, Object> invalid = new HashMap<>();
-    List<Object> invalid2 = new ArrayList<>();
-    invalid2.add(new SomeClass());
-    invalid.put("bar",invalid2);
-    JsonObject object = new JsonObject(invalid);
+    SomeClass invalid = new SomeClass();
+    JsonObject object = new JsonObject(Collections.singletonMap("bar", Collections.singletonList(invalid)));
     try {
       object.copy();
       fail();
     } catch (IllegalStateException e) {
       // OK
     }
+    object = object.copy(SomeClass.CLONER);
+    assertTrue(object.getJsonArray("bar").getValue(0) instanceof SomeClass);
+    assertNotSame(object.getJsonArray("bar").getValue(0), invalid);
   }
 
-  class SomeClass {
+  static class SomeClass {
+    static final Function<Object, ?> CLONER = o -> {
+      assertTrue(o instanceof SomeClass);
+      return new SomeClass();
+    };
   }
 
   @Test