You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@johnzon.apache.org by st...@apache.org on 2017/09/24 14:26:21 UTC

johnzon git commit: JOHNZON-135 deserialise JsonPointers back to previously mapped objects

Repository: johnzon
Updated Branches:
  refs/heads/master 0091f2019 -> 0f693d825


JOHNZON-135 deserialise JsonPointers back to previously mapped objects


Project: http://git-wip-us.apache.org/repos/asf/johnzon/repo
Commit: http://git-wip-us.apache.org/repos/asf/johnzon/commit/0f693d82
Tree: http://git-wip-us.apache.org/repos/asf/johnzon/tree/0f693d82
Diff: http://git-wip-us.apache.org/repos/asf/johnzon/diff/0f693d82

Branch: refs/heads/master
Commit: 0f693d8257ea70d13517bbacc1e92a499aa3df6f
Parents: 0091f20
Author: Mark Struberg <st...@apache.org>
Authored: Sun Sep 24 15:29:08 2017 +0200
Committer: Mark Struberg <st...@apache.org>
Committed: Sun Sep 24 16:25:52 2017 +0200

----------------------------------------------------------------------
 .../johnzon/mapper/MappingParserImpl.java       |  86 +++++++----
 .../mapper/internal/JsonPointerTracker.java     |  60 ++++++++
 .../johnzon/mapper/CircularExceptionTest.java   |  36 -----
 .../johnzon/mapper/CircularObjectsTest.java     | 153 +++++++++++++++++++
 .../mapper/internal/JsonPointerTrackerTest.java |  39 +++++
 5 files changed, 307 insertions(+), 67 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/johnzon/blob/0f693d82/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParserImpl.java
----------------------------------------------------------------------
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParserImpl.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParserImpl.java
index 7e3d574..4d111db 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParserImpl.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParserImpl.java
@@ -23,6 +23,7 @@ import org.apache.johnzon.mapper.converter.CharacterConverter;
 import org.apache.johnzon.mapper.converter.EnumConverter;
 import org.apache.johnzon.mapper.internal.AdapterKey;
 import org.apache.johnzon.mapper.internal.ConverterAdapter;
+import org.apache.johnzon.mapper.internal.JsonPointerTracker;
 import org.apache.johnzon.mapper.reflection.JohnzonParameterizedType;
 
 import javax.json.JsonArray;
@@ -89,6 +90,13 @@ public class MappingParserImpl implements MappingParser {
 
     private final JsonReader jsonReader;
 
+    /**
+     * Used for de-referencing JsonPointers during deserialisation.
+     * key: JsonPointer
+     * value: already deserialised Object
+     */
+    private Map<String, Object> jsonPointers = new HashMap<>();
+
 
     public MappingParserImpl(MapperConfig config, Mappings mappings, JsonReader jsonReader) {
         this.config = config;
@@ -123,7 +131,7 @@ public class MappingParserImpl implements MappingParser {
             return (T) jsonValue;
         }
         if (JsonObject.class.isInstance(jsonValue)) {
-            return (T) buildObject(targetType, JsonObject.class.cast(jsonValue), applyObjectConverter);
+            return (T) buildObject(targetType, JsonObject.class.cast(jsonValue), applyObjectConverter, new JsonPointerTracker(null, "/"));
         }
         if (JsonString.class.isInstance(jsonValue) && (targetType == String.class || targetType == Object.class)) {
             return (T) JsonString.class.cast(jsonValue).getString();
@@ -152,7 +160,7 @@ public class MappingParserImpl implements MappingParser {
 
             if (Class.class.isInstance(targetType) && ((Class) targetType).isArray()) {
                 final Class componentType = ((Class) targetType).getComponentType();
-                return (T) buildArrayWithComponentType(jsonArray, componentType, config.findAdapter(componentType));
+                return (T) buildArrayWithComponentType(jsonArray, componentType, config.findAdapter(componentType), new JsonPointerTracker(null, "/"));
             }
             if (ParameterizedType.class.isInstance(targetType)) {
 
@@ -163,10 +171,10 @@ public class MappingParserImpl implements MappingParser {
                 }
 
                 final Type arg = pt.getActualTypeArguments()[0];
-                return (T) mapCollection(mapping, jsonArray, Class.class.isInstance(arg) ? config.findAdapter(Class.class.cast(arg)) : null);
+                return (T) mapCollection(mapping, jsonArray, Class.class.isInstance(arg) ? config.findAdapter(Class.class.cast(arg)) : null, new JsonPointerTracker(null, "/"));
             }
             if (Object.class == targetType) {
-                return (T) new ArrayList(asList(Object[].class.cast(buildArrayWithComponentType(jsonArray, Object.class, null))));
+                return (T) new ArrayList(asList(Object[].class.cast(buildArrayWithComponentType(jsonArray, Object.class, null, new JsonPointerTracker(null, "/")))));
             }
         }
         if (JsonValue.NULL.equals(jsonValue)) {
@@ -182,7 +190,7 @@ public class MappingParserImpl implements MappingParser {
     }
 
 
-    private Object buildObject(final Type inType, final JsonObject object, final boolean applyObjectConverter) {
+    private Object buildObject(final Type inType, final JsonObject object, final boolean applyObjectConverter, JsonPointerTracker jsonPointer) {
         Type type = inType;
         if (inType == Object.class) {
             type = new JohnzonParameterizedType(Map.class, String.class, Object.class);
@@ -241,7 +249,7 @@ public class MappingParserImpl implements MappingParser {
                             } else if (JsonString.class.isInstance(jsonValue) && any) {
                                 map.put(value.getKey(), JsonString.class.cast(jsonValue).getString());
                             } else {
-                                map.put(convertTo(keyType, value.getKey()), toObject(null, jsonValue, fieldArgTypes[1], null));
+                                map.put(convertTo(keyType, value.getKey()), toObject(null, jsonValue, fieldArgTypes[1], null, jsonPointer));
                             }
                         }
                         return map;
@@ -250,7 +258,7 @@ public class MappingParserImpl implements MappingParser {
             } else if (Map.class == type || HashMap.class == type || LinkedHashMap.class == type) {
                 final LinkedHashMap<String, Object> map = new LinkedHashMap<String, Object>();
                 for (final Map.Entry<String, JsonValue> value : object.entrySet()) {
-                    map.put(value.getKey(), toObject(null, value.getValue(), Object.class, null));
+                    map.put(value.getKey(), toObject(null, value.getValue(), Object.class, null, jsonPointer));
                 }
                 return map;
             }
@@ -280,8 +288,15 @@ public class MappingParserImpl implements MappingParser {
             }
         }
 
-        final Object t = classMapping.factory.getParameterTypes().length == 0 ?
-                classMapping.factory.create(null) : classMapping.factory.create(createParameters(classMapping, object));
+        Object t;
+        if (classMapping.factory.getParameterTypes().length == 0) {
+            t = classMapping.factory.create(null);
+        } else {
+            t = classMapping.factory.create(createParameters(classMapping, object, jsonPointer));
+        }
+        // store the new object under it's jsonPointer in case it gets referenced later
+        jsonPointers.put(jsonPointer.toString(), t);
+
         for (final Map.Entry<String, Mappings.Setter> setter : classMapping.setters.entrySet()) {
             final JsonValue jsonValue = object.get(setter.getKey());
             final Mappings.Setter value = setter.getValue();
@@ -308,7 +323,8 @@ public class MappingParserImpl implements MappingParser {
                         }
                     }
                 }
-                final Object convertedValue = toValue(existingInstance, jsonValue, value.converter, value.itemConverter, value.paramType, value.objectConverter);
+                final Object convertedValue = toValue(existingInstance, jsonValue, value.converter, value.itemConverter, value.paramType, value.objectConverter,
+                        new JsonPointerTracker(jsonPointer, setter.getKey()));
                 if (convertedValue != null) {
                     setterMethod.write(t, convertedValue);
                 }
@@ -319,7 +335,7 @@ public class MappingParserImpl implements MappingParser {
                 final String key = entry.getKey();
                 if (!classMapping.setters.containsKey(key)) {
                     try {
-                        classMapping.anySetter.invoke(t, key, toValue(null, entry.getValue(), null, null, Object.class, null));
+                        classMapping.anySetter.invoke(t, key, toValue(null, entry.getValue(), null, null, Object.class, null, new JsonPointerTracker(jsonPointer, entry.getKey())));
                     } catch (final IllegalAccessException e) {
                         throw new IllegalStateException(e);
                     } catch (final InvocationTargetException e) {
@@ -350,7 +366,7 @@ public class MappingParserImpl implements MappingParser {
         }
     }
 
-    private Object convertTo(final Adapter converter, final JsonValue jsonValue) {
+    private Object convertTo(final Adapter converter, final JsonValue jsonValue, JsonPointerTracker jsonPointer) {
         if (jsonValue.getValueType() == JsonValue.ValueType.OBJECT) {
 
             //X TODO maybe we can put this into MapperConfig?
@@ -361,7 +377,7 @@ public class MappingParserImpl implements MappingParser {
             final Object param;
             try {
                 Type to = adapterKey.getTo();
-                param = buildObject(to, JsonObject.class.cast(jsonValue), to instanceof Class);
+                param = buildObject(to, JsonObject.class.cast(jsonValue), to instanceof Class, jsonPointer);
             } catch (final Exception e) {
                 throw new MapperException(e);
             }
@@ -432,7 +448,7 @@ public class MappingParserImpl implements MappingParser {
 
 
     private Object toObject(final Object baseInstance, final JsonValue jsonValue,
-                            final Type type, final Adapter itemConverter) {
+                            final Type type, final Adapter itemConverter, JsonPointerTracker jsonPointer) {
         if (jsonValue == null || JsonValue.NULL.equals(jsonValue)) {
             return null;
         }
@@ -475,13 +491,14 @@ public class MappingParserImpl implements MappingParser {
             final Object object = buildObject(
                     baseInstance != null ? baseInstance.getClass() : (
                             typedAdapter ? TypeAwareAdapter.class.cast(itemConverter).getTo() : type),
-                    JsonObject.class.cast(jsonValue), type instanceof Class);
+                    JsonObject.class.cast(jsonValue), type instanceof Class,
+                    jsonPointer);
             return typedAdapter ? itemConverter.to(object) : object;
         } else if (JsonArray.class.isInstance(jsonValue)) {
             if (JsonArray.class == type || JsonStructure.class == type) {
                 return jsonValue;
             }
-            return buildArray(type, JsonArray.class.cast(jsonValue), itemConverter);
+            return buildArray(type, JsonArray.class.cast(jsonValue), itemConverter, jsonPointer);
         } else if (JsonNumber.class.isInstance(jsonValue)) {
             if (JsonNumber.class == type) {
                 return jsonValue;
@@ -527,7 +544,13 @@ public class MappingParserImpl implements MappingParser {
 
             final String string = JsonString.class.cast(jsonValue).getString();
             if (itemConverter == null) {
-                return convertTo(Class.class.cast(type), string);
+                // check whether we have a jsonPointer to a previously deserialised object
+                Object o = jsonPointers.get(string);
+                if (o != null) {
+                    return o;
+                } else {
+                    return convertTo(Class.class.cast(type), string);
+                }
             } else {
                 return itemConverter.to(string);
             }
@@ -536,40 +559,40 @@ public class MappingParserImpl implements MappingParser {
         throw new MapperException("Unable to parse " + jsonValue + " to " + type);
     }
 
-    private Object buildArray(final Type type, final JsonArray jsonArray, final Adapter itemConverter) {
+    private Object buildArray(final Type type, final JsonArray jsonArray, final Adapter itemConverter, final JsonPointerTracker jsonPointer) {
         if (Class.class.isInstance(type)) {
             final Class clazz = Class.class.cast(type);
             if (clazz.isArray()) {
                 final Class<?> componentType = clazz.getComponentType();
-                return buildArrayWithComponentType(jsonArray, componentType, itemConverter);
+                return buildArrayWithComponentType(jsonArray, componentType, itemConverter, jsonPointer);
             }
         }
 
         if (ParameterizedType.class.isInstance(type)) {
             final Mappings.CollectionMapping mapping = mappings.findCollectionMapping(ParameterizedType.class.cast(type));
             if (mapping != null) {
-                return mapCollection(mapping, jsonArray, itemConverter);
+                return mapCollection(mapping, jsonArray, itemConverter, jsonPointer);
             }
         }
 
         if (Object.class == type) {
-            return buildArray(ANY_LIST, jsonArray, null);
+            return buildArray(ANY_LIST, jsonArray, null, jsonPointer);
         }
 
         throw new UnsupportedOperationException("type " + type + " not supported");
     }
 
-    private Object buildArrayWithComponentType(final JsonArray jsonArray, final Class<?> componentType, final Adapter itemConverter) {
+    private Object buildArrayWithComponentType(final JsonArray jsonArray, final Class<?> componentType, final Adapter itemConverter, JsonPointerTracker jsonPointer) {
         final Object array = Array.newInstance(componentType, jsonArray.size());
         int i = 0;
         for (final JsonValue value : jsonArray) {
-            Array.set(array, i++, toObject(null, value, componentType, itemConverter));
+            Array.set(array, i++, toObject(null, value, componentType, itemConverter, jsonPointer));
         }
         return array;
     }
 
     private <T> Collection<T> mapCollection(final Mappings.CollectionMapping mapping, final JsonArray jsonArray,
-                                            final Adapter itemConverter) {
+                                            final Adapter itemConverter, JsonPointerTracker jsonPointer) {
         final Collection collection;
 
         if (SortedSet.class == mapping.raw || NavigableSet.class == mapping.raw || TreeSet.class == mapping.raw) {
@@ -591,7 +614,7 @@ public class MappingParserImpl implements MappingParser {
         }
 
         for (final JsonValue value : jsonArray) {
-            collection.add(JsonValue.NULL.equals(value) ? null : toObject(null, value, mapping.arg, itemConverter));
+            collection.add(JsonValue.NULL.equals(value) ? null : toObject(null, value, mapping.arg, itemConverter, jsonPointer));
         }
 
         if (EnumSet.class == mapping.raw) {
@@ -609,25 +632,26 @@ public class MappingParserImpl implements MappingParser {
     }
 
 
-    private Object[] createParameters(final Mappings.ClassMapping mapping, final JsonObject object) {
+    private Object[] createParameters(final Mappings.ClassMapping mapping, final JsonObject object, JsonPointerTracker jsonPointer) {
         final int length = mapping.factory.getParameterTypes().length;
         final Object[] objects = new Object[length];
 
         for (int i = 0; i < length; i++) {
 
+            String paramName = mapping.factory.getParameterNames()[i];
             objects[i] = toValue(null,
-                    object.get(mapping.factory.getParameterNames()[i]),
+                    object.get(paramName),
                     mapping.factory.getParameterConverter()[i],
                     mapping.factory.getParameterItemConverter()[i],
                     mapping.factory.getParameterTypes()[i],
-                    null); //X TODO ObjectConverter in @JOhnzonConverter with Constructors!
+                    null, new JsonPointerTracker(jsonPointer, paramName)); //X TODO ObjectConverter in @JOhnzonConverter with Constructors!
         }
 
         return objects;
     }
 
     private Object toValue(final Object baseInstance, final JsonValue jsonValue, final Adapter converter,
-                           final Adapter itemConverter, final Type type, final ObjectConverter.Reader objectConverter) {
+                           final Adapter itemConverter, final Type type, final ObjectConverter.Reader objectConverter, JsonPointerTracker jsonPointer) {
 
         if (objectConverter != null) {
 
@@ -638,9 +662,9 @@ public class MappingParserImpl implements MappingParser {
             }
         }
 
-        return converter == null ? toObject(baseInstance, jsonValue, type, itemConverter)
+        return converter == null ? toObject(baseInstance, jsonValue, type, itemConverter, jsonPointer)
                 : jsonValue.getValueType() == JsonValue.ValueType.STRING ? converter.to(JsonString.class.cast(jsonValue).getString())
-                : convertTo(converter, jsonValue);
+                : convertTo(converter, jsonValue, jsonPointer);
     }
 
 

http://git-wip-us.apache.org/repos/asf/johnzon/blob/0f693d82/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/internal/JsonPointerTracker.java
----------------------------------------------------------------------
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/internal/JsonPointerTracker.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/internal/JsonPointerTracker.java
new file mode 100644
index 0000000..9aa713e
--- /dev/null
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/internal/JsonPointerTracker.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.johnzon.mapper.internal;
+
+/**
+ * Internal class to easily collect information about the 'depth' of a json object
+ * without having to eagerly construct it.
+ *
+ * For use in recursive generator and parser method calls to defer string operations.
+ */
+public class JsonPointerTracker {
+    private final JsonPointerTracker parent;
+    private final String currentNode;
+
+    private String jsonPointer;
+
+
+    /**
+     * @param parent or {@code null} if this is the root object
+     * @param currentNode the name of the attribute or "/" for the root object
+     */
+    public JsonPointerTracker(JsonPointerTracker parent, String currentNode) {
+        this.parent = parent;
+        this.currentNode = currentNode;
+    }
+
+    @Override
+    public String toString() {
+        if (jsonPointer == null) {
+            if (parent != null) {
+                if (parent.parent == null) {
+                    jsonPointer = "/" + currentNode;
+                } else {
+                    jsonPointer = parent.toString() + "/" + currentNode;
+                }
+            } else {
+                jsonPointer = "/";
+            }
+        }
+
+        return jsonPointer;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/johnzon/blob/0f693d82/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/CircularExceptionTest.java
----------------------------------------------------------------------
diff --git a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/CircularExceptionTest.java b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/CircularExceptionTest.java
index 17fe5f7..d6da4cb 100644
--- a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/CircularExceptionTest.java
+++ b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/CircularExceptionTest.java
@@ -18,7 +18,6 @@
  */
 package org.apache.johnzon.mapper;
 
-import org.junit.Assert;
 import org.junit.Test;
 
 import static org.junit.Assert.assertTrue;
@@ -34,39 +33,4 @@ public class CircularExceptionTest {
         assertTrue(serialized.contains("\"stackTrace\":[{"));
     }
 
-    @Test
-    public void testCyclicPerson() {
-        Person john = new Person("John");
-        Person marry = new Person("Marry");
-
-        john.setMarriedTo(marry);
-        marry.setMarriedTo(john);
-
-        String ser = new MapperBuilder().setAccessModeName("field").build().writeObjectAsString(john);
-        Assert.assertNotNull(ser);
-        assertTrue(ser.contains("\"name\":\"John\""));
-        assertTrue(ser.contains("\"marriedTo\":\"/\""));
-        assertTrue(ser.contains("\"name\":\"Marry\""));
-    }
-
-    public static class Person {
-        private final String name;
-        private Person marriedTo;
-
-        public Person(String name) {
-            this.name = name;
-        }
-
-        public String getName() {
-            return name;
-        }
-
-        public Person getMarriedTo() {
-            return marriedTo;
-        }
-
-        public void setMarriedTo(Person marriedTo) {
-            this.marriedTo = marriedTo;
-        }
-    }
 }

http://git-wip-us.apache.org/repos/asf/johnzon/blob/0f693d82/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/CircularObjectsTest.java
----------------------------------------------------------------------
diff --git a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/CircularObjectsTest.java b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/CircularObjectsTest.java
new file mode 100644
index 0000000..272cd45
--- /dev/null
+++ b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/CircularObjectsTest.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.johnzon.mapper;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Test serialising objects which contain the same Object multiple times,
+ * sometimes even with cycles.
+ */
+public class CircularObjectsTest {
+
+    @Test
+    public void testSimpleCyclicPerson() {
+        Person john = new Person("John");
+        Person marry = new Person("Marry");
+
+        john.setMarriedTo(marry);
+        marry.setMarriedTo(john);
+
+        Mapper mapper = new MapperBuilder().setAccessModeName("field").build();
+        String ser = mapper.writeObjectAsString(john);
+
+        assertNotNull(ser);
+        assertTrue(ser.contains("\"name\":\"John\""));
+        assertTrue(ser.contains("\"marriedTo\":\"/\""));
+        assertTrue(ser.contains("\"name\":\"Marry\""));
+
+        // and now de-serialise it back
+        Person john2 = mapper.readObject(ser, Person.class);
+        assertNotNull(john2);
+        assertEquals("John", john2.getName());
+
+        Person marry2 = john2.getMarriedTo();
+        assertNotNull(marry2);
+        assertEquals("Marry", marry2.getName());
+
+        assertEquals(john2, marry2.getMarriedTo());
+    }
+
+    @Test
+    public void testComplexCyclicPerson() {
+        Person karl = new Person("Karl");
+        Person andrea = new Person("Andrea");
+        Person lu = new Person("Lu");
+        Person sue = new Person("Sue");
+
+        karl.setMarriedTo(andrea);
+        karl.getKids().add(lu);
+        karl.getKids().add(sue);
+
+        andrea.setMarriedTo(karl);
+        andrea.getKids().add(lu);
+        andrea.getKids().add(sue);
+
+        lu.setFather(karl);
+        lu.setMother(andrea);
+
+        sue.setFather(karl);
+        sue.setMother(andrea);
+
+        Mapper mapper = new MapperBuilder().setAccessModeName("field").build();
+        String karlJson = mapper.writeObjectAsString(karl);
+        Person karl2 = mapper.readObject(karlJson, Person.class);
+        assertEquals("Karl", karl2.getName());
+        assertEquals("Andrea", karl2.getMarriedTo().getName());
+        assertEquals(karl2, karl2.getMarriedTo().getMarriedTo());
+        assertEquals(2, karl2.getKids().size());
+        assertEquals("Lu", karl2.getKids().get(0).getName());
+        assertEquals("Sue", karl2.getKids().get(1).getName());
+        assertEquals(2, karl2.getMarriedTo().getKids().size());
+        assertEquals("Lu", karl2.getMarriedTo().getKids().get(0).getName());
+        assertEquals("Sue", karl2.getMarriedTo().getKids().get(1).getName());
+    }
+
+    public static class Person {
+        private String name;
+        private Person marriedTo;
+        private Person mother;
+        private Person father;
+        private List<Person> kids = new ArrayList<>();
+
+        public Person() {
+        }
+
+        public Person(String name) {
+            this.name = name;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public Person getMarriedTo() {
+            return marriedTo;
+        }
+
+        public void setMarriedTo(Person marriedTo) {
+            this.marriedTo = marriedTo;
+        }
+
+        public Person getMother() {
+            return mother;
+        }
+
+        public void setMother(Person mother) {
+            this.mother = mother;
+        }
+
+        public Person getFather() {
+            return father;
+        }
+
+        public void setFather(Person father) {
+            this.father = father;
+        }
+
+        public List<Person> getKids() {
+            return kids;
+        }
+
+        public void setKids(List<Person> kids) {
+            this.kids = kids;
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/johnzon/blob/0f693d82/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/internal/JsonPointerTrackerTest.java
----------------------------------------------------------------------
diff --git a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/internal/JsonPointerTrackerTest.java b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/internal/JsonPointerTrackerTest.java
new file mode 100644
index 0000000..1bcc7f6
--- /dev/null
+++ b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/internal/JsonPointerTrackerTest.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.johnzon.mapper.internal;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class JsonPointerTrackerTest {
+
+
+    @Test
+    public void testJsonPointerTracker() {
+        JsonPointerTracker jptRoot = new JsonPointerTracker(null, "/");
+
+        Assert.assertEquals("/", jptRoot.toString());
+
+        JsonPointerTracker jptAttrL1 = new JsonPointerTracker(jptRoot, "attrL1");
+        JsonPointerTracker jptAttrL2 = new JsonPointerTracker(jptAttrL1, "attrL2");
+
+        Assert.assertEquals("/attrL1/attrL2", jptAttrL2.toString());
+        Assert.assertEquals("/attrL1", jptAttrL1.toString());
+    }
+}