You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@johnzon.apache.org by rm...@apache.org on 2021/06/20 14:28:47 UTC

[johnzon] branch master updated (87bbe67 -> c8e233e)

This is an automated email from the ASF dual-hosted git repository.

rmannibucau pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/johnzon.git.


    from 87bbe67  [JOHNZON-347] some adjustment for recent java versions, need some more love to be faster
     new cf2cd73  [JOHNZON-348] jsonb annotations support on records
     new c8e233e  [JOHNZON-349] trying to not do reflection until we know we can, dedup annotation case

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../org/apache/johnzon/jsonb/JsonbAccessMode.java  | 227 ++++++++++++++-------
 ...rter.java => JsonbOffsetDateTimeConverter.java} |  14 +-
 .../org/apache/johnzon/jsonb/CdiAdapterTest.java   |  99 +++++++--
 .../org/apache/johnzon/jsonb/JsonbRecordTest.java  |  43 ++--
 .../jsonschema/JsonSchemaValidatorFactory.java     |  11 +-
 .../johnzon/jsonschema/regex/JavascriptRegex.java  |   3 +-
 .../java/org/apache/johnzon/mapper/Mapper.java     |  42 +---
 .../org/apache/johnzon/mapper/MapperConfig.java    |   4 +-
 .../johnzon/mapper/MappingGeneratorImpl.java       |  66 +++---
 .../apache/johnzon/mapper/MappingParserImpl.java   |  86 ++++----
 .../java/org/apache/johnzon/mapper/Mappings.java   |  19 +-
 .../mapper/internal/JsonPointerTracker.java        |  15 +-
 .../apache/johnzon/mapper/CircularObjectsTest.java |  20 +-
 .../mapper/internal/JsonPointerTrackerTest.java    |   2 +-
 pom.xml                                            |   2 +-
 15 files changed, 394 insertions(+), 259 deletions(-)
 copy johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/converter/{JsonbZonedDateTimeConverter.java => JsonbOffsetDateTimeConverter.java} (65%)
 copy johnzon-mapper/src/test/java/org/apache/johnzon/mapper/RecordTest.java => johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/JsonbRecordTest.java (71%)

[johnzon] 01/02: [JOHNZON-348] jsonb annotations support on records

Posted by rm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rmannibucau pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/johnzon.git

commit cf2cd73c3b5ff85852ec34a7caeb483665506af3
Author: Romain Manni-Bucau <rm...@gmail.com>
AuthorDate: Sun Jun 20 16:27:23 2021 +0200

    [JOHNZON-348] jsonb annotations support on records
---
 .../org/apache/johnzon/jsonb/JsonbAccessMode.java  | 227 ++++++++++++++-------
 .../org/apache/johnzon/jsonb/JsonbRecordTest.java  |  87 ++++++++
 2 files changed, 236 insertions(+), 78 deletions(-)

diff --git a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JsonbAccessMode.java b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JsonbAccessMode.java
index cc03e7b..3204101 100644
--- a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JsonbAccessMode.java
+++ b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JsonbAccessMode.java
@@ -18,12 +18,62 @@
  */
 package org.apache.johnzon.jsonb;
 
-import static java.util.Arrays.asList;
-import static java.util.Optional.ofNullable;
-import static java.util.stream.Collectors.toList;
-import static java.util.stream.Collectors.toSet;
-import static org.apache.johnzon.mapper.reflection.Converters.matches;
+import org.apache.johnzon.core.Types;
+import org.apache.johnzon.jsonb.converter.JohnzonJsonbAdapter;
+import org.apache.johnzon.jsonb.converter.JsonbDateConverter;
+import org.apache.johnzon.jsonb.converter.JsonbLocalDateConverter;
+import org.apache.johnzon.jsonb.converter.JsonbLocalDateTimeConverter;
+import org.apache.johnzon.jsonb.converter.JsonbNumberConverter;
+import org.apache.johnzon.jsonb.converter.JsonbOffsetDateTimeConverter;
+import org.apache.johnzon.jsonb.converter.JsonbValueConverter;
+import org.apache.johnzon.jsonb.converter.JsonbZonedDateTimeConverter;
+import org.apache.johnzon.jsonb.order.PerHierarchyAndLexicographicalOrderFieldComparator;
+import org.apache.johnzon.jsonb.reflect.GenericArrayTypeImpl;
+import org.apache.johnzon.jsonb.serializer.JohnzonDeserializationContext;
+import org.apache.johnzon.jsonb.serializer.JohnzonSerializationContext;
+import org.apache.johnzon.jsonb.spi.JohnzonAdapterFactory;
+import org.apache.johnzon.mapper.Adapter;
+import org.apache.johnzon.mapper.Converter;
+import org.apache.johnzon.mapper.JohnzonAny;
+import org.apache.johnzon.mapper.JohnzonConverter;
+import org.apache.johnzon.mapper.JohnzonRecord;
+import org.apache.johnzon.mapper.MapperConverter;
+import org.apache.johnzon.mapper.MappingGenerator;
+import org.apache.johnzon.mapper.MappingParser;
+import org.apache.johnzon.mapper.ObjectConverter;
+import org.apache.johnzon.mapper.TypeAwareAdapter;
+import org.apache.johnzon.mapper.access.AccessMode;
+import org.apache.johnzon.mapper.access.BaseAccessMode;
+import org.apache.johnzon.mapper.access.FieldAccessMode;
+import org.apache.johnzon.mapper.access.FieldAndMethodAccessMode;
+import org.apache.johnzon.mapper.access.Meta;
+import org.apache.johnzon.mapper.access.MethodAccessMode;
+import org.apache.johnzon.mapper.converter.ReversedAdapter;
+import org.apache.johnzon.mapper.internal.AdapterKey;
+import org.apache.johnzon.mapper.internal.ConverterAdapter;
 
+import javax.json.JsonBuilderFactory;
+import javax.json.JsonValue;
+import javax.json.bind.JsonbException;
+import javax.json.bind.adapter.JsonbAdapter;
+import javax.json.bind.annotation.JsonbCreator;
+import javax.json.bind.annotation.JsonbDateFormat;
+import javax.json.bind.annotation.JsonbNillable;
+import javax.json.bind.annotation.JsonbNumberFormat;
+import javax.json.bind.annotation.JsonbProperty;
+import javax.json.bind.annotation.JsonbPropertyOrder;
+import javax.json.bind.annotation.JsonbTransient;
+import javax.json.bind.annotation.JsonbTypeAdapter;
+import javax.json.bind.annotation.JsonbTypeDeserializer;
+import javax.json.bind.annotation.JsonbTypeSerializer;
+import javax.json.bind.config.PropertyNamingStrategy;
+import javax.json.bind.config.PropertyOrderStrategy;
+import javax.json.bind.config.PropertyVisibilityStrategy;
+import javax.json.bind.serializer.JsonbDeserializer;
+import javax.json.bind.serializer.JsonbSerializer;
+import javax.json.spi.JsonProvider;
+import javax.json.stream.JsonGenerator;
+import javax.json.stream.JsonParserFactory;
 import java.io.Closeable;
 import java.io.IOException;
 import java.lang.annotation.Annotation;
@@ -39,10 +89,12 @@ import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
 import java.time.ZonedDateTime;
 import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.Date;
 import java.util.HashMap;
@@ -65,60 +117,15 @@ import java.util.function.Supplier;
 import java.util.stream.Collector;
 import java.util.stream.Stream;
 
-import javax.json.JsonBuilderFactory;
-import javax.json.JsonValue;
-import javax.json.bind.JsonbException;
-import javax.json.bind.adapter.JsonbAdapter;
-import javax.json.bind.annotation.JsonbCreator;
-import javax.json.bind.annotation.JsonbDateFormat;
-import javax.json.bind.annotation.JsonbNillable;
-import javax.json.bind.annotation.JsonbNumberFormat;
-import javax.json.bind.annotation.JsonbProperty;
-import javax.json.bind.annotation.JsonbPropertyOrder;
-import javax.json.bind.annotation.JsonbTransient;
-import javax.json.bind.annotation.JsonbTypeAdapter;
-import javax.json.bind.annotation.JsonbTypeDeserializer;
-import javax.json.bind.annotation.JsonbTypeSerializer;
-import javax.json.bind.config.PropertyNamingStrategy;
-import javax.json.bind.config.PropertyOrderStrategy;
-import javax.json.bind.config.PropertyVisibilityStrategy;
-import javax.json.bind.serializer.JsonbDeserializer;
-import javax.json.bind.serializer.JsonbSerializer;
-import javax.json.spi.JsonProvider;
-import javax.json.stream.JsonGenerator;
-import javax.json.stream.JsonParserFactory;
-
-import org.apache.johnzon.core.Types;
-import org.apache.johnzon.jsonb.converter.JohnzonJsonbAdapter;
-import org.apache.johnzon.jsonb.converter.JsonbDateConverter;
-import org.apache.johnzon.jsonb.converter.JsonbLocalDateConverter;
-import org.apache.johnzon.jsonb.converter.JsonbLocalDateTimeConverter;
-import org.apache.johnzon.jsonb.converter.JsonbNumberConverter;
-import org.apache.johnzon.jsonb.converter.JsonbValueConverter;
-import org.apache.johnzon.jsonb.converter.JsonbZonedDateTimeConverter;
-import org.apache.johnzon.jsonb.order.PerHierarchyAndLexicographicalOrderFieldComparator;
-import org.apache.johnzon.jsonb.reflect.GenericArrayTypeImpl;
-import org.apache.johnzon.jsonb.serializer.JohnzonDeserializationContext;
-import org.apache.johnzon.jsonb.serializer.JohnzonSerializationContext;
-import org.apache.johnzon.jsonb.spi.JohnzonAdapterFactory;
-import org.apache.johnzon.mapper.Adapter;
-import org.apache.johnzon.mapper.Converter;
-import org.apache.johnzon.mapper.JohnzonAny;
-import org.apache.johnzon.mapper.JohnzonConverter;
-import org.apache.johnzon.mapper.MapperConverter;
-import org.apache.johnzon.mapper.MappingGenerator;
-import org.apache.johnzon.mapper.MappingParser;
-import org.apache.johnzon.mapper.ObjectConverter;
-import org.apache.johnzon.mapper.TypeAwareAdapter;
-import org.apache.johnzon.mapper.access.AccessMode;
-import org.apache.johnzon.mapper.access.BaseAccessMode;
-import org.apache.johnzon.mapper.access.FieldAccessMode;
-import org.apache.johnzon.mapper.access.FieldAndMethodAccessMode;
-import org.apache.johnzon.mapper.access.Meta;
-import org.apache.johnzon.mapper.access.MethodAccessMode;
-import org.apache.johnzon.mapper.converter.ReversedAdapter;
-import org.apache.johnzon.mapper.internal.AdapterKey;
-import org.apache.johnzon.mapper.internal.ConverterAdapter;
+import static java.util.Arrays.asList;
+import static java.util.Comparator.comparing;
+import static java.util.Optional.ofNullable;
+import static java.util.function.Function.identity;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toMap;
+import static java.util.stream.Collectors.toSet;
+import static org.apache.johnzon.mapper.reflection.Converters.matches;
+import static org.apache.johnzon.mapper.reflection.Records.isRecord;
 
 public class JsonbAccessMode implements AccessMode, Closeable {
     private final PropertyNamingStrategy naming;
@@ -210,6 +217,10 @@ public class JsonbAccessMode implements AccessMode, Closeable {
             }
             factory = m;
         }
+        final boolean record = isRecord(clazz) || Meta.getAnnotation(clazz, JohnzonRecord.class) != null;
+        if (constructor == null && record) {
+            constructor = findRecordConstructor(clazz).orElse(null);
+        }
         if (constructor == null && factory == null) {
             invalidConstructorForDeserialization = Stream.of(clazz.getDeclaredConstructors())
                     .anyMatch(it -> it.getParameterCount() == 0 &&
@@ -223,7 +234,8 @@ public class JsonbAccessMode implements AccessMode, Closeable {
                         throw new JsonbException("Missing @JsonbCreator argument");
                     }
                 } :
-                args -> {};
+                args -> {
+                };
         final Type[] types;
         final String[] params;
         final Adapter<?, ?>[] converters;
@@ -239,7 +251,13 @@ public class JsonbAccessMode implements AccessMode, Closeable {
             int i = 0;
             for (final Parameter parameter : (finalConstructor == null ? finalFactory : finalConstructor).getParameters()) {
                 final JsonbProperty property = getAnnotation(parameter, JsonbProperty.class);
-                params[i] = property != null ? property.value() : parameter.getName();
+                params[i] = property != null ?
+                        property.value() :
+                        (record ?
+                                ofNullable(parameter.getAnnotation(JohnzonRecord.Name.class))
+                                        .map(JohnzonRecord.Name::value)
+                                        .orElseGet(parameter::getName) :
+                                parameter.getName());
 
                 final JsonbTypeAdapter adapter = getAnnotation(parameter, JsonbTypeAdapter.class);
                 final JsonbDateFormat dateFormat = getAnnotation(parameter, JsonbDateFormat.class);
@@ -252,7 +270,7 @@ public class JsonbAccessMode implements AccessMode, Closeable {
                     validateAnnotations(parameter, adapter, dateFormat, numberFormat, johnzonConverter);
 
                     try {
-                        if (adapter != null) {
+                        if (adapter != null || dateFormat != null || numberFormat != null) {
                             final Adapter converter = toConverter(
                                     this.types, parameter.getType(), adapter, dateFormat, numberFormat);
                             if (matches(parameter.getParameterizedType(), converter)) {
@@ -282,10 +300,11 @@ public class JsonbAccessMode implements AccessMode, Closeable {
 
         if (constructor == null && factory == null && !invalidConstructorForDeserialization) {
             final Stream<Function<AnnotatedElement, String>> jsonbFn = Stream.of(this::getJsonbProperty);
-            return delegate.findFactory(
+            final Factory delegateFactory = delegate.findFactory(
                     clazz,
                     (parameterNameExtractors == null ?
-                        jsonbFn : Stream.concat(jsonbFn, Stream.of(parameterNameExtractors))).toArray(Function[]::new));
+                            jsonbFn : Stream.concat(jsonbFn, Stream.of(parameterNameExtractors))).toArray(Function[]::new));
+            return delegateFactory;
         }
         if (constructor != null || invalidConstructorForDeserialization) {
             return constructorFactory(finalConstructor, invalidConstructorForDeserialization ? (Consumer<Object[]>) objects -> {
@@ -295,6 +314,17 @@ public class JsonbAccessMode implements AccessMode, Closeable {
         return methodFactory(clazz, finalFactory, factoryValidator, types, params, converters, itemConverters, objectConverters);
     }
 
+    private Optional<Constructor<?>> findRecordConstructor(final Class<?> clazz) {
+        return Stream.of(clazz.getDeclaredConstructors())
+                .max(comparing(Constructor::getParameterCount))
+                .map(c -> {
+                    if (!c.isAccessible()) {
+                        c.setAccessible(true);
+                    }
+                    return c;
+                });
+    }
+
     private String getJsonbProperty(final AnnotatedElement a) {
         final JsonbProperty p = Meta.getAnnotation(a, JsonbProperty.class);
         return p != null ? p.value() : null;
@@ -448,6 +478,8 @@ public class JsonbAccessMode implements AccessMode, Closeable {
                 converter = new ConverterAdapter<>(new JsonbLocalDateConverter(dateFormat), LocalDate.class);
             } else if (ZonedDateTime.class == type) {
                 converter = new ConverterAdapter<>(new JsonbZonedDateTimeConverter(dateFormat), ZonedDateTime.class);
+            } else if (OffsetDateTime.class == type) {
+                converter = new ConverterAdapter<>(new JsonbOffsetDateTimeConverter(dateFormat), OffsetDateTime.class);
             } else { // can happen if set on the class, todo: refine the checks
                 converter = null; // todo: should we fallback on numberformat?
             }
@@ -467,15 +499,54 @@ public class JsonbAccessMode implements AccessMode, Closeable {
     public Map<String, Reader> findReaders(final Class<?> clazz) {
         final Map<String, Reader> readers = delegate.findReaders(clazz);
 
+        final boolean record = isRecord(clazz) || Meta.getAnnotation(clazz, JohnzonRecord.class) != null;
+        final Map<String, Parameter> recordParams = record ?
+                findRecordConstructor(clazz)
+                        .map(c -> Stream.of(c.getParameters())
+                                .collect(toMap(p -> ofNullable(p.getAnnotation(JohnzonRecord.Name.class))
+                                        .map(JohnzonRecord.Name::value)
+                                        .orElseGet(p::getName), identity())))
+                        .orElseGet(Collections::emptyMap) :
+                null;
         final Comparator<String> keyComparator = fieldComparator(clazz);
         final Map<String, Reader> result = keyComparator == null ? new HashMap<>() : new TreeMap<>(keyComparator);
         for (final Map.Entry<String, Reader> entry : readers.entrySet()) {
             final Reader initialReader = entry.getValue();
+            final DecoratedType annotations = record ? new DecoratedType() {
+                private final Parameter parameter = recordParams.get(entry.getKey());
+
+                @Override
+                public Type getType() {
+                    return initialReader.getType();
+                }
+
+                @Override
+                public <T extends Annotation> T getAnnotation(final Class<T> clazz) {
+                    final T annotation = initialReader.getAnnotation(clazz);
+                    return annotation == null && parameter != null ? parameter.getAnnotation(clazz) : annotation;
+                }
+
+                @Override
+                public <T extends Annotation> T getClassOrPackageAnnotation(final Class<T> clazz) {
+                    final T annotation = parameter == null ? null : parameter.getAnnotation(clazz);
+                    return annotation == null ? initialReader.getClassOrPackageAnnotation(clazz) : annotation;
+                }
+
+                @Override
+                public Adapter<?, ?> findConverter() {
+                    return initialReader.findConverter();
+                }
+
+                @Override
+                public boolean isNillable(final boolean globalConfig) {
+                    return initialReader.isNillable(globalConfig);
+                }
+            } : initialReader;
             if (isTransient(initialReader, visibility)) {
                 validateAnnotationsOnTransientField(initialReader);
                 continue;
             }
-            if (initialReader.getAnnotation(JohnzonAny.class) != null) {
+            if (annotations.getAnnotation(JohnzonAny.class) != null) {
                 continue;
             }
 
@@ -524,18 +595,18 @@ public class JsonbAccessMode implements AccessMode, Closeable {
                     final Object[] optionals = Object[].class.cast(finalReader.read(i));
                     return optionals == null ?
                             null : Stream.of(optionals)
-                                .map(Optional.class::cast)
-                                .map(o -> o.orElse(null))
-                                .toArray();
+                            .map(Optional.class::cast)
+                            .map(o -> o.orElse(null))
+                            .toArray();
                 };
             } else {
                 type = readerType;
                 reader = finalReader::read;
             }
 
-            final WriterConverters writerConverters = new WriterConverters(initialReader, types);
-            final JsonbProperty property = initialReader.getAnnotation(JsonbProperty.class);
-            final JsonbNillable nillable = initialReader.getClassOrPackageAnnotation(JsonbNillable.class);
+            final WriterConverters writerConverters = new WriterConverters(annotations, types);
+            final JsonbProperty property = annotations.getAnnotation(JsonbProperty.class);
+            final JsonbNillable nillable = annotations.getClassOrPackageAnnotation(JsonbNillable.class);
             final boolean isNillable = isNillable(property, nillable);
             final String key = property == null || property.value().isEmpty() ? naming.translateName(entry.getKey()) : property.value();
             if (result.put(key, new Reader() {
@@ -642,8 +713,8 @@ public class JsonbAccessMode implements AccessMode, Closeable {
                 writer = (i, value) -> {
                     if (value != null) {
                         finalWriter.write(i, Stream.of(Object[].class.cast(value))
-                            .map(Optional::ofNullable)
-                            .toArray(Optional[]::new));
+                                .map(Optional::ofNullable)
+                                .toArray(Optional[]::new));
                     }
                 };
             } else {
@@ -960,8 +1031,8 @@ public class JsonbAccessMode implements AccessMode, Closeable {
                                                 Set.class.isAssignableFrom(
                                                         types.asClass(parameterizedType.getRawType())) ? toSet() : toList();
                                         fn = (json, mp) -> json.asJsonArray().stream()
-                                               .map(i -> mapItem(i, paramType, mp, jsonbDeserializer))
-                                               .collect(collector);
+                                                .map(i -> mapItem(i, paramType, mp, jsonbDeserializer))
+                                                .collect(collector);
                                     }
                                 }
                             }
@@ -1045,7 +1116,7 @@ public class JsonbAccessMode implements AccessMode, Closeable {
                 try {
                     MapperConverter mapperConverter = johnzonConverter.value().newInstance();
                     if (mapperConverter instanceof Converter) {
-                        converter = new ConverterAdapter<>((Converter) mapperConverter, reader.getType()) ;
+                        converter = new ConverterAdapter<>((Converter) mapperConverter, reader.getType());
                     } else if (mapperConverter instanceof ObjectConverter.Writer) {
                         writer = (ObjectConverter.Writer) mapperConverter;
                     }
@@ -1074,8 +1145,8 @@ public class JsonbAccessMode implements AccessMode, Closeable {
 
     private boolean hasRawType(final Type type) {
         return Class.class.isInstance(type) ||
-            (ParameterizedType.class.isInstance(type) &&
-                    Class.class.isInstance(ParameterizedType.class.cast(type).getRawType()));
+                (ParameterizedType.class.isInstance(type) &&
+                        Class.class.isInstance(ParameterizedType.class.cast(type).getRawType()));
     }
 
     private Class<?> getRawType(final Type type) { // only intended to be used after hasRawType check
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/JsonbRecordTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/JsonbRecordTest.java
new file mode 100644
index 0000000..81e28e3
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/JsonbRecordTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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.jsonb;
+
+import org.apache.johnzon.jsonb.test.JsonbRule;
+import org.apache.johnzon.mapper.JohnzonRecord;
+import org.junit.Rule;
+import org.junit.Test;
+
+import javax.json.bind.annotation.JsonbProperty;
+import java.util.Objects;
+
+import static org.junit.Assert.assertEquals;
+
+public class JsonbRecordTest {
+    @Rule
+    public final JsonbRule jsonb = new JsonbRule();
+
+    @Test
+    public void roundTrip() {
+        final Record ref = new Record(119, "Santa");
+        final String expectedJson = "{\"_name\":\"Santa\",\"age\":119}";
+        assertEquals(expectedJson, jsonb.toJson(ref));
+        assertEquals(ref, jsonb.fromJson(expectedJson, Record.class));
+    }
+
+    @JohnzonRecord
+    public static class Record {
+        private final int age;
+        private final String name;
+
+        public Record(@JohnzonRecord.Name("age") final int age,
+                      @JohnzonRecord.Name("name") @JsonbProperty("_name") final String name) { // simulate custom constructor
+            this.age = age;
+            this.name = name;
+        }
+
+        public int age() {
+            return age;
+        }
+
+        public String name() {
+            return name;
+        }
+
+        @Override
+        public String toString() {
+            return "Record{" +
+                    "age=" + age +
+                    ", name='" + name + '\'' +
+                    '}';
+        }
+
+        @Override
+        public boolean equals(final Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+            final Record record = Record.class.cast(o);
+            return age == record.age && Objects.equals(name, record.name);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(age, name);
+        }
+    }
+}

[johnzon] 02/02: [JOHNZON-349] trying to not do reflection until we know we can, dedup annotation case

Posted by rm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rmannibucau pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/johnzon.git

commit c8e233e2f3c54037b8aa5d5d9bf165b550cc9641
Author: Romain Manni-Bucau <rm...@gmail.com>
AuthorDate: Sun Jun 20 16:28:42 2021 +0200

    [JOHNZON-349] trying to not do reflection until we know we can, dedup annotation case
---
 .../converter/JsonbOffsetDateTimeConverter.java    | 42 +++++++++
 .../org/apache/johnzon/jsonb/CdiAdapterTest.java   | 99 +++++++++++++++++-----
 .../jsonschema/JsonSchemaValidatorFactory.java     | 11 ++-
 .../johnzon/jsonschema/regex/JavascriptRegex.java  |  3 +-
 .../java/org/apache/johnzon/mapper/Mapper.java     | 42 +++------
 .../org/apache/johnzon/mapper/MapperConfig.java    |  4 +-
 .../johnzon/mapper/MappingGeneratorImpl.java       | 66 +++++++++------
 .../apache/johnzon/mapper/MappingParserImpl.java   | 86 +++++++++----------
 .../java/org/apache/johnzon/mapper/Mappings.java   | 19 ++---
 .../mapper/internal/JsonPointerTracker.java        | 15 +++-
 .../apache/johnzon/mapper/CircularObjectsTest.java | 20 +++--
 .../mapper/internal/JsonPointerTrackerTest.java    |  2 +-
 pom.xml                                            |  2 +-
 13 files changed, 258 insertions(+), 153 deletions(-)

diff --git a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/converter/JsonbOffsetDateTimeConverter.java b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/converter/JsonbOffsetDateTimeConverter.java
new file mode 100644
index 0000000..a78c1ac
--- /dev/null
+++ b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/converter/JsonbOffsetDateTimeConverter.java
@@ -0,0 +1,42 @@
+/*
+ * 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.jsonb.converter;
+
+import javax.json.bind.annotation.JsonbDateFormat;
+import java.time.Instant;
+import java.time.OffsetDateTime;
+import java.time.ZoneId;
+
+public class JsonbOffsetDateTimeConverter extends JsonbDateConverterBase<OffsetDateTime> {
+    private static final ZoneId UTC = ZoneId.of("UTC");
+
+    public JsonbOffsetDateTimeConverter(final JsonbDateFormat dateFormat) {
+        super(dateFormat);
+    }
+
+    @Override
+    public String toString(final OffsetDateTime instance) {
+        return formatter == null ? instance.toString() : instance.format(formatter);
+    }
+
+    @Override
+    public OffsetDateTime fromString(final String text) {
+        return formatter == null ? OffsetDateTime.ofInstant(Instant.ofEpochMilli(Long.parseLong(text)), UTC) : OffsetDateTime.parse(text, formatter);
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/CdiAdapterTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/CdiAdapterTest.java
index d071541..2ef3742 100644
--- a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/CdiAdapterTest.java
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/CdiAdapterTest.java
@@ -18,55 +18,112 @@
  */
 package org.apache.johnzon.jsonb;
 
+import org.apache.johnzon.jsonb.cdi.JohnzonCdiExtension;
 import org.apache.webbeans.config.WebBeansContext;
 import org.apache.webbeans.config.WebBeansFinder;
+import org.apache.webbeans.corespi.DefaultSingletonService;
+import org.apache.webbeans.corespi.scanner.xbean.CdiArchive;
 import org.apache.webbeans.corespi.se.DefaultScannerService;
 import org.apache.webbeans.lifecycle.StandaloneLifeCycle;
 import org.apache.webbeans.proxy.OwbNormalScopeProxy;
+import org.apache.webbeans.service.ClassLoaderProxyService;
+import org.apache.webbeans.spi.BeanArchiveService;
 import org.apache.webbeans.spi.ContainerLifecycle;
+import org.apache.webbeans.spi.DefiningClassService;
+import org.apache.webbeans.spi.LoaderService;
+import org.apache.webbeans.spi.ResourceInjectionService;
 import org.apache.webbeans.spi.ScannerService;
+import org.apache.webbeans.spi.api.ResourceReference;
 import org.apache.webbeans.util.WebBeansUtil;
-import org.apache.xbean.finder.AnnotationFinder;
-import org.apache.xbean.finder.archive.ClassesArchive;
+import org.apache.webbeans.xml.DefaultBeanArchiveInformation;
 import org.junit.Test;
 
 import javax.enterprise.context.ApplicationScoped;
+import javax.enterprise.inject.spi.Extension;
 import javax.inject.Inject;
 import javax.json.bind.Jsonb;
 import javax.json.bind.JsonbBuilder;
 import javax.json.bind.adapter.JsonbAdapter;
 import javax.json.bind.annotation.JsonbTypeAdapter;
+import java.lang.annotation.Annotation;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.Properties;
 
-import static java.util.Collections.singletonMap;
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
 
 public class CdiAdapterTest {
     @Test
-    public void run() {
-        WebBeansFinder.clearInstances(WebBeansUtil.getCurrentClassLoader());
+    public void run() throws Exception {
+        final ClassLoader currentClassLoader = WebBeansUtil.getCurrentClassLoader();
+        WebBeansFinder.clearInstances(currentClassLoader);
+        final Map<Class<?>, Object> services = new HashMap<>();
+        services.put(ScannerService.class, new DefaultScannerService() {
+            @Override
+            protected void configure() {
+                initFinder();
+                final DefaultBeanArchiveInformation information = new DefaultBeanArchiveInformation("file://foo");
+                information.setBeanDiscoveryMode(BeanArchiveService.BeanDiscoveryMode.ALL);
+                try {
+                    archive.classesByUrl().put(information.getBdaUrl(), new CdiArchive.FoundClasses(
+                            URI.create(information.getBdaUrl()).toURL(), asList(
+                            Service.class.getName(), ModelAdapter.class.getName()),
+                            information));
+                } catch (final MalformedURLException e) {
+                    throw new IllegalStateException(e);
+                }
+            }
+        });
+        services.put(ResourceInjectionService.class, new ResourceInjectionService() {
+            @Override
+            public void injectJavaEEResources(final Object managedBeanInstance) {
+                System.out.println();
+            }
+
+            @Override
+            public <X, T extends Annotation> X getResourceReference(final ResourceReference<X, T> resourceReference) {
+                return null;
+            }
+
+            @Override
+            public void clear() {
+                // no-op
+            }
+        });
+        services.put(LoaderService.class, new LoaderService() {
+            @Override
+            public <T> List<T> load(final Class<T> serviceType, final ClassLoader loader) {
+                if (Extension.class == serviceType) {
+                    return singletonList(serviceType.cast(new JohnzonCdiExtension()));
+                }
+                return Collections.emptyList();
+            }
+
+            @Override
+            public <T> List<T> load(final Class<T> serviceType) {
+                return emptyList();
+            }
+        });
+        final Properties properties = new Properties();
+        properties.setProperty(DefiningClassService.class.getName(), ClassLoaderProxyService.class.getName());
+        final WebBeansContext webBeansContext = new WebBeansContext(services, properties);
+        DefaultSingletonService.class.cast(WebBeansFinder.getSingletonService()).register(
+                currentClassLoader, webBeansContext);
         final ContainerLifecycle testLifecycle = new StandaloneLifeCycle();
-        new WebBeansContext(singletonMap(
-                ScannerService.class, new DefaultScannerService() {
-                    @Override
-                    protected AnnotationFinder initFinder() {
-                        return new AnnotationFinder(new ClassesArchive(Service.class, ModelAdapter.class));
-                    }
-                }), new Properties());
         testLifecycle.startApplication(null);
-        try {
-            Jsonb jsonb = JsonbBuilder.create();
+        try (final Jsonb jsonb = JsonbBuilder.create()) {
             assertEquals("{\"model\":\"5\"}", jsonb.toJson(new Root(new Model(5))));
-            try {
-                AutoCloseable.class.cast(jsonb).close();
-            } catch (final Exception e) {
-                fail(e.getMessage());
-            }
         } finally {
             testLifecycle.stopApplication(null);
-            WebBeansFinder.clearInstances(WebBeansUtil.getCurrentClassLoader());
+            WebBeansFinder.clearInstances(currentClassLoader);
         }
     }
 
diff --git a/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/JsonSchemaValidatorFactory.java b/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/JsonSchemaValidatorFactory.java
index 2fe4e27..14103bc 100644
--- a/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/JsonSchemaValidatorFactory.java
+++ b/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/JsonSchemaValidatorFactory.java
@@ -36,6 +36,7 @@ import java.util.stream.StreamSupport;
 import javax.json.JsonObject;
 import javax.json.JsonValue;
 
+import org.apache.johnzon.jsonschema.regex.JavaRegex;
 import org.apache.johnzon.jsonschema.regex.JavascriptRegex;
 import org.apache.johnzon.jsonschema.spi.ValidationContext;
 import org.apache.johnzon.jsonschema.spi.ValidationExtension;
@@ -76,7 +77,15 @@ public class JsonSchemaValidatorFactory implements AutoCloseable {
     private final List<ValidationExtension> extensions = new ArrayList<>();
 
     // js is closer to default and actually most used in the industry
-    private final AtomicReference<Function<String, Predicate<CharSequence>>> regexFactory = new AtomicReference<>(JavascriptRegex::new);
+    private final AtomicReference<Function<String, Predicate<CharSequence>>> regexFactory = new AtomicReference<>(this::newRegexFactory);
+
+    private Predicate<CharSequence> newRegexFactory(final String regex) {
+        try {
+            return new JavascriptRegex(regex);
+        } catch (final RuntimeException re) {
+            return new JavaRegex(regex);
+        }
+    }
 
     public JsonSchemaValidatorFactory() {
         extensions.addAll(createDefaultValidations());
diff --git a/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/regex/JavascriptRegex.java b/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/regex/JavascriptRegex.java
index 9b83389..6ec93cf 100644
--- a/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/regex/JavascriptRegex.java
+++ b/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/regex/JavascriptRegex.java
@@ -18,12 +18,11 @@
  */
 package org.apache.johnzon.jsonschema.regex;
 
-import java.util.function.Predicate;
-
 import javax.script.Bindings;
 import javax.script.ScriptEngine;
 import javax.script.ScriptEngineManager;
 import javax.script.ScriptException;
+import java.util.function.Predicate;
 
 public class JavascriptRegex implements Predicate<CharSequence> {
 
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mapper.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mapper.java
index b74c105..022eaee 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mapper.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mapper.java
@@ -114,8 +114,7 @@ public class Mapper implements Closeable {
 
     public <T> void writeArray(final Collection<T> object, final Writer stream) {
         try (final JsonGenerator generator = generatorFactory.createGenerator(stream(stream))) {
-            boolean dedup = Boolean.TRUE.equals(config.isDeduplicateObjects());
-            writeObject(object, generator, null, dedup ? new JsonPointerTracker(null, "/") : null);
+            writeObject(object, generator, null, config.isDeduplicateObjects() ? JsonPointerTracker.ROOT : null);
         }
     }
 
@@ -125,8 +124,7 @@ public class Mapper implements Closeable {
 
     public <T> void writeIterable(final Iterable<T> object, final Writer stream) {
         try (final JsonGenerator generator = generatorFactory.createGenerator(stream(stream))) {
-            boolean dedup = Boolean.TRUE.equals(config.isDeduplicateObjects());
-            writeObject(object, generator, null, dedup ? new JsonPointerTracker(null, "/") : null);
+            writeObject(object, generator, null, config.isDeduplicateObjects() ? JsonPointerTracker.ROOT : null);
         }
     }
 
@@ -162,8 +160,7 @@ public class Mapper implements Closeable {
             return provider.createValue(BigInteger.class.cast(object));
         }
         final JsonObjectGenerator objectGenerator = new JsonObjectGenerator(builderFactory);
-        writeObject(object, objectGenerator, null,
-                isDedup(object.getClass()) ? new JsonPointerTracker(null, "/") : null);
+        writeObject(object, objectGenerator, null, null);
         return objectGenerator.getResult();
     }
 
@@ -192,8 +189,7 @@ public class Mapper implements Closeable {
     }
 
     public void writeObjectWithGenerator(final Object object, final JsonGenerator generator) {
-        writeObject(object, generator, null,
-                isDedup(object.getClass()) ? new JsonPointerTracker(null, "/") : null);
+        writeObject(object, generator, null, null);
     }
 
     public void writeObject(final Object object, final OutputStream stream) {
@@ -205,9 +201,10 @@ public class Mapper implements Closeable {
         writeObject(object, new OutputStreamWriter(stream, charset));
     }
 
-    private void writeObject(final Object object, final JsonGenerator generator, final Collection<String> ignored, JsonPointerTracker jsonPointer) {
-        final MappingGeneratorImpl mappingGenerator = new MappingGeneratorImpl(config, generator, mappings, jsonPointer != null);
-        mappingGenerator.doWriteObject(object, generator, true, ignored, jsonPointer);
+    private void writeObject(final Object object, final JsonGenerator generator, final Collection<String> ignored,
+                             final JsonPointerTracker tracker) {
+        final MappingGeneratorImpl mappingGenerator = new MappingGeneratorImpl(config, generator, mappings);
+        mappingGenerator.doWriteObject(object, generator, true, ignored, tracker);
     }
 
     public String writeArrayAsString(final Collection<?> instance) {
@@ -263,7 +260,7 @@ public class Mapper implements Closeable {
             public void close() {
                 // no-op
             }
-        }, isDedup(clazz)).readObject(clazz);
+        }, null).readObject(clazz);
     }
 
     public <T> T readObject(final String string, final Type clazz) {
@@ -370,26 +367,7 @@ public class Mapper implements Closeable {
 
 
     private <T> T mapObject(final Type clazz, final JsonReader reader) {
-        return new MappingParserImpl(config, mappings, reader, isDedup(clazz)).readObject(clazz);
-    }
-
-    private boolean isDedup(final Type clazz) {
-        if (clazz instanceof Class &&
-                JsonValue.class != clazz && JsonStructure.class != clazz &&
-                JsonObject.class != clazz && JsonArray.class != clazz) {
-            Boolean dedup = config.isDeduplicateObjects();
-            if (dedup == null) {
-                // TODO: never call it more than once per clazz, should be done after once ClassMapping is obtained, not here!
-                //       -> revisit org.apache.johnzon.mapper.Mappings.findOrCreateClassMapping (isPrimitive should drop)
-                //       -> revisit org.apache.johnzon.mapper.access.FieldAccessMode.isIgnored(java.lang.String, java.lang.Class<?>)
-                Mappings.ClassMapping classMapping = mappings.findOrCreateClassMapping(clazz);
-                if (classMapping != null) {
-                    dedup = classMapping.isDeduplicateObjects();
-                }
-            }
-            return dedup != null ? dedup : false;
-        }
-        return false;
+        return new MappingParserImpl(config, mappings, reader, null).readObject(clazz);
     }
 
 
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperConfig.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperConfig.java
index eea1716..4d8bceb 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperConfig.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperConfig.java
@@ -412,8 +412,8 @@ public /* DON'T MAKE IT HIDDEN */ class MapperConfig implements Cloneable {
         return useBigDecimalForFloats;
     }
 
-    public Boolean isDeduplicateObjects() {
-        return deduplicateObjects;
+    public boolean isDeduplicateObjects() {
+        return Boolean.TRUE.equals(deduplicateObjects);
     }
 
     public boolean isSupportEnumContainerDeserialization() {
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingGeneratorImpl.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingGeneratorImpl.java
index d4d103c..bdfba2a 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingGeneratorImpl.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingGeneratorImpl.java
@@ -19,6 +19,7 @@
 package org.apache.johnzon.mapper;
 
 import static java.util.Collections.emptyList;
+import static java.util.Collections.emptyMap;
 import static java.util.stream.Collectors.toList;
 
 import org.apache.johnzon.mapper.internal.JsonPointerTracker;
@@ -45,19 +46,12 @@ public class MappingGeneratorImpl implements MappingGenerator {
     private final MapperConfig config;
     private final JsonGenerator generator;
     private final Mappings mappings;
-
-    private final Boolean isDeduplicateObjects;
     private Map<Object, String> jsonPointers;
 
-
-    MappingGeneratorImpl(MapperConfig config, JsonGenerator jsonGenerator, final Mappings mappings, Boolean isDeduplicateObjects) {
+    MappingGeneratorImpl(MapperConfig config, JsonGenerator jsonGenerator, final Mappings mappings) {
         this.config = config;
         this.generator = jsonGenerator;
         this.mappings = mappings;
-
-        this.isDeduplicateObjects = isDeduplicateObjects;
-
-        this.jsonPointers = isDeduplicateObjects ?  new HashMap<>() : Collections.emptyMap();
     }
 
     @Override
@@ -76,7 +70,7 @@ public class MappingGeneratorImpl implements MappingGenerator {
             try {
                 if (Map.class.isInstance(object)) {
                     writeValue(Map.class, false, false, false, false, true, null, key, object,
-                            null, emptyList(), isDeduplicateObjects ? new JsonPointerTracker(null, "/") : null, generator);
+                            null, emptyList(), isDedup() ? JsonPointerTracker.ROOT : null, generator);
                 } else if(writePrimitives(key, objectClass, object, generator)) {
                     // no-op
                 } else if (Enum.class.isAssignableFrom(objectClass)) {
@@ -85,10 +79,10 @@ public class MappingGeneratorImpl implements MappingGenerator {
                     generator.write(key, adaptedValue);
                 } else if (objectClass.isArray()) {
                     writeValue(Map.class, false, false, true, false, false, null, key, object,
-                            null, emptyList(), isDeduplicateObjects ? new JsonPointerTracker(null, "/") : null, generator);
+                            null, emptyList(), isDedup() ? JsonPointerTracker.ROOT : null, generator);
                 } else if (Iterable.class.isInstance(object)) {
                     writeValue(Map.class, false, false, false, true, false, null, key, object,
-                            null, emptyList(), isDeduplicateObjects ? new JsonPointerTracker(null, "/") : null, generator);
+                            null, emptyList(), isDedup() ? JsonPointerTracker.ROOT : null, generator);
                 } else {
                     final ObjectConverter.Writer objectConverter = config.findObjectConverterWriter(objectClass);
                     if (objectConverter != null) {
@@ -98,7 +92,7 @@ public class MappingGeneratorImpl implements MappingGenerator {
                         dynamicMappingGenerator.flushIfNeeded();
                     } else {
                         writeValue(objectClass, false, false, false, false, false, null, key, object,
-                                null, emptyList(), isDeduplicateObjects ? new JsonPointerTracker(null, "/") : null, generator);
+                                null, emptyList(), isDedup() ? JsonPointerTracker.ROOT : null, generator);
                     }
                 }
             } catch (final InvocationTargetException | IllegalAccessException e) {
@@ -115,11 +109,15 @@ public class MappingGeneratorImpl implements MappingGenerator {
         } else if (object instanceof JsonValue) {
             generator.write((JsonValue) object);
         } else {
-            doWriteObject(object, generator, false, null, isDeduplicateObjects ? new JsonPointerTracker(null, "/") : null);
+            doWriteObject(object, generator, false, null, isDedup() ? JsonPointerTracker.ROOT : null);
         }
         return this;
     }
 
+    private boolean isDedup() {
+        return config.isDeduplicateObjects() || (jsonPointers != null && jsonPointers != Collections.<Object, String>emptyMap());
+    }
+
     public void doWriteObject(Object object, JsonGenerator generator, boolean writeBody, final Collection<String> ignoredProperties,
                               JsonPointerTracker jsonPointer) {
 
@@ -184,6 +182,14 @@ public class MappingGeneratorImpl implements MappingGenerator {
                     dynamicMappingGenerator.flushIfNeeded();
                 }
             } else {
+                if (classMapping == null) { // will be created anyway now so force it and if it has an adapter respect it
+                    final Mappings.ClassMapping mapping = mappings.findOrCreateClassMapping(objectClass);
+                    if (mapping != null && mapping.adapter != null) {
+                        final Object result = mapping.adapter.from(object);
+                        doWriteObject(result, generator, writeBody, ignoredProperties, jsonPointer);
+                        return;
+                    }
+                }
                 if (writeBody) {
                     generator.writeStartObject();
                 }
@@ -345,17 +351,23 @@ public class MappingGeneratorImpl implements MappingGenerator {
     private boolean doWriteObjectBody(final Object object, final Collection<String> ignored,
                                       final JsonPointerTracker jsonPointer, final JsonGenerator generator)
             throws IllegalAccessException, InvocationTargetException {
-
-        if (jsonPointer != null) {
-            jsonPointers.put(object, jsonPointer.toString());
-        }
-
         final Class<?> objectClass = object.getClass();
         final Mappings.ClassMapping classMapping = mappings.findOrCreateClassMapping(objectClass);
         if (classMapping == null) {
             throw new MapperException("No mapping for " + objectClass.getName());
         }
 
+        if (jsonPointers == null) {
+            if (classMapping.deduplicateObjects || config.isDeduplicateObjects()) {
+                jsonPointers = new HashMap<>();
+                jsonPointers.putIfAbsent(object, jsonPointer == null ? "/" : jsonPointer.toString());
+            } else {
+                jsonPointers = emptyMap();
+            }
+        } else if (isDedup()) {
+            jsonPointers.putIfAbsent(object, jsonPointer == null ? "/" : jsonPointer.toString());
+        }
+
         if (classMapping.writer != null) {
             final DynamicMappingGenerator gen = new DynamicMappingGenerator.SkipEnclosingWriteEnd(this, null, generator);
             classMapping.writer.writeJson(object, gen);
@@ -409,7 +421,7 @@ public class MappingGeneratorImpl implements MappingGenerator {
                         val,
                         getter.objectConverter,
                         getter.ignoreNested,
-                        isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, getterEntry.getKey()) : null,
+                        isDedup() ? new JsonPointerTracker(jsonPointer, getterEntry.getKey()) : null,
                         generator);
             }
         }
@@ -527,7 +539,7 @@ public class MappingGeneratorImpl implements MappingGenerator {
         generator.writeStartArray(key);
         while (iterator.hasNext()) {
             final Object o = iterator.next();
-            String valJsonPointer = jsonPointers.get(o);
+            String valJsonPointer = jsonPointers == null ? null : jsonPointers.get(o);
             if (valJsonPointer != null) {
                 // write JsonPointer instead of the original object
                 writePrimitives(valJsonPointer);
@@ -544,7 +556,7 @@ public class MappingGeneratorImpl implements MappingGenerator {
                     dynamicMappingGenerator.flushIfNeeded();
                 } else {
                     writeItem(itemConverter != null ? itemConverter.from(o) : o, ignoredProperties,
-                            isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, i) : null);
+                            isDedup() ? new JsonPointerTracker(jsonPointer, i) : null);
                 }
             }
             i++;
@@ -648,21 +660,23 @@ public class MappingGeneratorImpl implements MappingGenerator {
             Object[] oArrayValue = (Object[]) arrayValue;
             for (int i = 0; i < length; i++) {
                 final Object o = oArrayValue[i];
-                writeItem(itemConverter != null ? itemConverter.from(o) : o, ignoredProperties, null);
+                writeItem(itemConverter != null ? itemConverter.from(o) : o, ignoredProperties,
+                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null);
             }
         } else {
             // must be object arrays
             for (int i = 0; i < length; i++) {
                 Object[] oArrayValue = (Object[]) arrayValue;
                 final Object o = oArrayValue[i];
-                String valJsonPointer = jsonPointers.get(o);
+                String valJsonPointer = jsonPointers == null ? null : jsonPointers.get(o);
                 if (valJsonPointer != null) {
                     // write the JsonPointer as String natively
                     generator.write(valJsonPointer);
                 } else if (o instanceof JsonValue) {
                     generator.write((JsonValue) o);
                 } else {
-                    writeItem(itemConverter != null ? itemConverter.from(o) : o, ignoredProperties, isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, i) : null);
+                    writeItem(itemConverter != null ? itemConverter.from(o) : o, ignoredProperties,
+                            isDedup() ? new JsonPointerTracker(jsonPointer, i) : null);
                 }
             }
         }
@@ -682,7 +696,7 @@ public class MappingGeneratorImpl implements MappingGenerator {
                     writeArray(o.getClass(), null, null, o, ignoredProperties, jsonPointer);
                 }
             } else {
-                String valJsonPointer = jsonPointers.get(o);
+                String valJsonPointer = jsonPointers == null ? null : jsonPointers.get(o);
                 if (valJsonPointer != null) {
                     // write the JsonPointer instead
                     generator.write(valJsonPointer);
@@ -706,7 +720,7 @@ public class MappingGeneratorImpl implements MappingGenerator {
                     if (t == null) {
                         generator.writeNull();
                     } else {
-                        writeItem(t, ignoredProperties, isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, i) : null);
+                        writeItem(t, ignoredProperties, isDedup() ? new JsonPointerTracker(jsonPointer, i) : null);
                     }
                 }
                 i++;
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 80ca4ad..e376e87 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
@@ -98,7 +98,6 @@ public class MappingParserImpl implements MappingParser {
 
     private final MapperConfig config;
     private final Mappings mappings;
-    private final boolean isDeduplicateObjects;
 
     private final JsonReader jsonReader;
 
@@ -110,19 +109,10 @@ public class MappingParserImpl implements MappingParser {
     private Map<String, Object> jsonPointers;
 
 
-    public MappingParserImpl(MapperConfig config, Mappings mappings, JsonReader jsonReader, boolean isDeduplicateObjects) {
+    public MappingParserImpl(MapperConfig config, Mappings mappings, JsonReader jsonReader, Map<String, Object> jsonPointers) {
         this.config = config;
         this.mappings = mappings;
-
         this.jsonReader = jsonReader;
-
-        this.isDeduplicateObjects = isDeduplicateObjects;
-
-        if (isDeduplicateObjects) {
-            jsonPointers = new HashMap<>();
-        } else {
-            jsonPointers = Collections.emptyMap();
-        }
     }
 
 
@@ -150,8 +140,7 @@ public class MappingParserImpl implements MappingParser {
         if (JsonObject.class.isInstance(jsonValue)) {
             return (T) buildObject(
                     targetType, JsonObject.class.cast(jsonValue), applyObjectConverter,
-                    isDeduplicateObjects ? new JsonPointerTracker(null, "/") : null,
-                    skippedConverters);
+                    null, skippedConverters);
         }
         if (JsonString.class.isInstance(jsonValue)) {
             if ((targetType == String.class || targetType == Object.class)) {
@@ -222,7 +211,7 @@ public class MappingParserImpl implements MappingParser {
                 if (asClass.isArray()) {
                     final Class componentType = asClass.getComponentType();
                     return (T) buildArrayWithComponentType(jsonArray, componentType, config.findAdapter(componentType),
-                            isDeduplicateObjects ? new JsonPointerTracker(null, "/") : null, Object.class);
+                            isDedup() ? JsonPointerTracker.ROOT : null, Object.class);
                 }
                 if (Collection.class.isAssignableFrom(asClass)) {
                     return readObject(jsonValue, new JohnzonParameterizedType(asClass, Object.class), applyObjectConverter, skippedConverters);
@@ -238,11 +227,11 @@ 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,
-                        null, isDeduplicateObjects ? new JsonPointerTracker(null, "/") : null, Object.class);
+                        null, isDedup() ? JsonPointerTracker.ROOT : null, Object.class);
             }
             if (Object.class == targetType) {
                 return (T) new ArrayList(asList(Object[].class.cast(buildArrayWithComponentType(jsonArray, Object.class, null,
-                        isDeduplicateObjects ? new JsonPointerTracker(null, "/") : null, Object.class))));
+                        isDedup() ? JsonPointerTracker.ROOT : null, Object.class))));
             }
         }
         if (NULL == valueType) {
@@ -258,6 +247,9 @@ public class MappingParserImpl implements MappingParser {
         throw new IllegalArgumentException("Unsupported " + jsonValue + " for type " + targetType);
     }
 
+    private boolean isDedup() {
+        return jsonPointers != Collections.<String, Object>emptyMap();
+    }
 
     private Object buildObject(final Type inType, final JsonObject object, final boolean applyObjectConverter,
                                final JsonPointerTracker jsonPointer, final Collection<Class<?>> skippedConverters) {
@@ -389,8 +381,15 @@ public class MappingParserImpl implements MappingParser {
             t = classMapping.factory.create(createParameters(classMapping, object, jsonPointer));
         }
         // store the new object under it's jsonPointer in case it gets referenced later
-        if (isDeduplicateObjects) {
-            jsonPointers.put(jsonPointer.toString(), t);
+        if (jsonPointers == null) {
+            if (classMapping.deduplicateObjects || config.isDeduplicateObjects()) {
+                jsonPointers = new HashMap<>();
+                jsonPointers.put(jsonPointer == null ? "/" : jsonPointer.toString(), t);
+            } else {
+                jsonPointers = Collections.emptyMap();
+            }
+        } else if (isDedup()) {
+            jsonPointers.put(jsonPointer == null ? "/" : jsonPointer.toString(), t);
         }
 
         for (final Map.Entry<String, JsonValue> jsonEntry : object.entrySet()) {
@@ -428,7 +427,7 @@ public class MappingParserImpl implements MappingParser {
                 final Object convertedValue = toValue(
                         existingInstance, jsonValue, value.converter, value.itemConverter,
                         value.paramType, value.objectConverter,
-                        new JsonPointerTracker(jsonPointer, jsonEntry.getKey()), inType);
+                        isDedup() ? new JsonPointerTracker(jsonPointer, jsonEntry.getKey()) : null, inType);
                 if (convertedValue != null) {
                     setterMethod.write(t, convertedValue);
                 }
@@ -442,7 +441,7 @@ public class MappingParserImpl implements MappingParser {
                         classMapping.anySetter.invoke(t, key,
                                 toValue(null, entry.getValue(), null, null,
                                         classMapping.anySetter.getGenericParameterTypes()[1], null,
-                                isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, entry.getKey()) : null, type));
+                                isDedup() ? new JsonPointerTracker(jsonPointer, entry.getKey()) : null, type));
                     } catch (final IllegalAccessException e) {
                         throw new IllegalStateException(e);
                     } catch (final InvocationTargetException e) {
@@ -457,7 +456,7 @@ public class MappingParserImpl implements MappingParser {
                     .filter(it -> !classMapping.setters.containsKey(it.getKey()))
                     .collect(toMap(Map.Entry::getKey, e -> toValue(null, e.getValue(), null, null,
                             ParameterizedType.class.cast(classMapping.anyField.getGenericType()).getActualTypeArguments()[1], null,
-                            isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, e.getKey()) : null, tRef))));
+                            isDedup() ? new JsonPointerTracker(jsonPointer, e.getKey()) : null, tRef))));
             } catch (final IllegalAccessException e) {
                 throw new IllegalStateException(e);
             }
@@ -724,16 +723,15 @@ public class MappingParserImpl implements MappingParser {
             final String string = JsonString.class.cast(jsonValue).getString();
             if (itemConverter == null) {
                 // check whether we have a jsonPointer to a previously deserialised object
-                if (!String.class.equals(type)) {
-                    Object o = jsonPointers.get(string);
+                if (isDedup() && !String.class.equals(type)) {
+                    Object o = jsonPointers == null ? null : jsonPointers.get(string);
                     if (o != null) {
                         return o;
                     }
                 }
                 return convertTo(type, string);
-            } else {
-                return itemConverter.to(string);
             }
+            return itemConverter.to(string);
         }
 
         throw new MapperException("Unable to parse " + jsonValue + " to " + type);
@@ -810,7 +808,7 @@ public class MappingParserImpl implements MappingParser {
             int i = 0;
             for (final JsonValue value : jsonArray) {
                 array[i] = (boolean) toObject(null, value, componentType, itemConverter,
-                        isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
+                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                 i++;
             }
             return array;
@@ -820,7 +818,7 @@ public class MappingParserImpl implements MappingParser {
             int i = 0;
             for (final JsonValue value : jsonArray) {
                 array[i] = (byte) toObject(null, value, componentType, itemConverter,
-                        isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
+                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                 i++;
             }
             return array;
@@ -830,7 +828,7 @@ public class MappingParserImpl implements MappingParser {
             int i = 0;
             for (final JsonValue value : jsonArray) {
                 array[i] = (char) toObject(null, value, componentType, itemConverter,
-                        isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
+                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                 i++;
             }
             return array;
@@ -840,7 +838,7 @@ public class MappingParserImpl implements MappingParser {
             int i = 0;
             for (final JsonValue value : jsonArray) {
                 array[i] = (short) toObject(null, value, componentType, itemConverter,
-                        isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
+                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                 i++;
             }
             return array;
@@ -850,7 +848,7 @@ public class MappingParserImpl implements MappingParser {
             int i = 0;
             for (final JsonValue value : jsonArray) {
                 array[i] = (int) toObject(null, value, componentType, itemConverter,
-                        isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
+                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                 i++;
             }
             return array;
@@ -860,7 +858,7 @@ public class MappingParserImpl implements MappingParser {
             int i = 0;
             for (final JsonValue value : jsonArray) {
                 array[i] = (long) toObject(null, value, componentType, itemConverter,
-                        isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
+                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                 i++;
             }
             return array;
@@ -870,7 +868,7 @@ public class MappingParserImpl implements MappingParser {
             int i = 0;
             for (final JsonValue value : jsonArray) {
                 array[i] = (float) toObject(null, value, componentType, itemConverter,
-                        isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
+                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                 i++;
             }
             return array;
@@ -880,7 +878,7 @@ public class MappingParserImpl implements MappingParser {
             int i = 0;
             for (final JsonValue value : jsonArray) {
                 array[i] = (double) toObject(null, value, componentType, itemConverter,
-                        isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
+                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                 i++;
             }
             return array;
@@ -892,7 +890,7 @@ public class MappingParserImpl implements MappingParser {
             int i = 0;
             for (final JsonValue value : jsonArray) {
                 array[i] = (Boolean) toObject(null, value, componentType, itemConverter,
-                        isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
+                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                 i++;
             }
             return array;
@@ -902,7 +900,7 @@ public class MappingParserImpl implements MappingParser {
             int i = 0;
             for (final JsonValue value : jsonArray) {
                 array[i] = (Byte) toObject(null, value, componentType, itemConverter,
-                        isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
+                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                 i++;
             }
             return array;
@@ -912,7 +910,7 @@ public class MappingParserImpl implements MappingParser {
             int i = 0;
             for (final JsonValue value : jsonArray) {
                 array[i] = (Character) toObject(null, value, componentType, itemConverter,
-                        isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
+                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                 i++;
             }
             return array;
@@ -922,7 +920,7 @@ public class MappingParserImpl implements MappingParser {
             int i = 0;
             for (final JsonValue value : jsonArray) {
                 array[i] = (Short) toObject(null, value, componentType, itemConverter,
-                        isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
+                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                 i++;
             }
             return array;
@@ -932,7 +930,7 @@ public class MappingParserImpl implements MappingParser {
             int i = 0;
             for (final JsonValue value : jsonArray) {
                 array[i] = (Integer) toObject(null, value, componentType, itemConverter,
-                        isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
+                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                 i++;
             }
             return array;
@@ -942,7 +940,7 @@ public class MappingParserImpl implements MappingParser {
             int i = 0;
             for (final JsonValue value : jsonArray) {
                 array[i] = (Long) toObject(null, value, componentType, itemConverter,
-                        isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
+                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                 i++;
             }
             return array;
@@ -952,7 +950,7 @@ public class MappingParserImpl implements MappingParser {
             int i = 0;
             for (final JsonValue value : jsonArray) {
                 array[i] = (Float) toObject(null, value, componentType, itemConverter,
-                        isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
+                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                 i++;
             }
             return array;
@@ -962,7 +960,7 @@ public class MappingParserImpl implements MappingParser {
             int i = 0;
             for (final JsonValue value : jsonArray) {
                 array[i] = (Double) toObject(null, value, componentType, itemConverter,
-                        isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
+                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                 i++;
             }
             return array;
@@ -973,7 +971,7 @@ public class MappingParserImpl implements MappingParser {
         int i = 0;
         for (final JsonValue value : jsonArray) {
             Array.set(array, i, toObject(null, value, componentType, itemConverter,
-                    isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, i) : null, rootType));
+                    isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType));
             i++;
         }
         return array;
@@ -1009,7 +1007,7 @@ public class MappingParserImpl implements MappingParser {
             collection.add(JsonValue.NULL.equals(value)
                     ? null
                     : toValue(null, value, null, itemConverter, mapping.arg, objectConverter,
-                    isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, i) : null, rootType));
+                    isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType));
             i++;
         }
 
@@ -1045,7 +1043,7 @@ public class MappingParserImpl implements MappingParser {
                     mapping.factory.getParameterItemConverter()[i],
                     parameterType,
                     mapping.factory.getObjectConverter()[i],
-                    isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, paramName) : null,
+                    isDedup() ? new JsonPointerTracker(jsonPointer, paramName) : null,
                     mapping.clazz); //X TODO ObjectConverter in @JohnzonConverter with Constructors!
             if (objects[i] == null) {
                 objects[i] = getPrimitiveDefault(parameterType);
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mappings.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mappings.java
index 23141be..686f285 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mappings.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mappings.java
@@ -76,10 +76,7 @@ public class Mappings {
         public final Field anyField;
         public final Method mapAdder;
         public final Class<?> mapAdderType;
-
-
-        private Boolean deduplicateObjects;
-        private boolean deduplicationEvaluated = false;
+        public boolean deduplicateObjects;
 
         protected ClassMapping(final Class<?> clazz, final AccessMode.Factory factory,
                                final Map<String, Getter> getters, final Map<String, Setter> setters,
@@ -99,17 +96,15 @@ public class Mappings {
             this.anyField = anyField;
             this.mapAdder = mapAdder;
             this.mapAdderType = mapAdder == null ? null : mapAdder.getParameterTypes()[1];
+            this.deduplicateObjects = isDeduplicateObjects();
         }
 
-        public Boolean isDeduplicateObjects() {
-            if (!deduplicationEvaluated) {
-                JohnzonDeduplicateObjects jdo = clazz.getAnnotation(JohnzonDeduplicateObjects.class);
-                if (jdo != null){
-                    deduplicateObjects = jdo.value();
-                }
-                deduplicationEvaluated = true;
+        private Boolean isDeduplicateObjects() {
+            final JohnzonDeduplicateObjects jdo = clazz.getAnnotation(JohnzonDeduplicateObjects.class);
+            if (jdo != null){
+                return jdo.value();
             }
-            return deduplicateObjects;
+            return false;
         }
 
     }
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
index 201a609..fe376c0 100644
--- 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
@@ -27,6 +27,13 @@ import org.apache.johnzon.core.JsonPointerUtil;
  * For use in recursive generator and parser method calls to defer string operations.
  */
 public class JsonPointerTracker {
+    public static final JsonPointerTracker ROOT = new JsonPointerTracker(null, null) {
+        @Override
+        public String toString() {
+            return "/";
+        }
+    };
+
     private final JsonPointerTracker parent;
     private final String currentNode;
 
@@ -55,13 +62,13 @@ public class JsonPointerTracker {
     public String toString() {
         if (jsonPointer == null) {
             if (parent != null) {
-                if (parent.parent == null) {
+                jsonPointer = (parent != ROOT ? parent + "/" : "/") + JsonPointerUtil.encode(currentNode);
+            } else {
+                if (currentNode != null) {
                     jsonPointer = "/" + JsonPointerUtil.encode(currentNode);
                 } else {
-                    jsonPointer = parent.toString() + "/" + JsonPointerUtil.encode(currentNode);
+                    jsonPointer = "/";
                 }
-            } else {
-                jsonPointer = "/";
             }
         }
 
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
index b4c8af6..98665a0 100644
--- a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/CircularObjectsTest.java
+++ b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/CircularObjectsTest.java
@@ -20,6 +20,7 @@ package org.apache.johnzon.mapper;
 
 import java.io.StringReader;
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.List;
 
 import org.junit.Test;
@@ -42,13 +43,13 @@ public class CircularObjectsTest {
         john.setMarriedTo(marry);
         marry.setMarriedTo(john);
 
-        Mapper mapper = new MapperBuilder().setAccessModeName("field").setDeduplicateObjects(true).build();
+        Mapper mapper = new MapperBuilder()
+                .setAccessModeName("field")
+                .setDeduplicateObjects(true)
+                .setAttributeOrder(Comparator.naturalOrder())
+                .build();
         String ser = mapper.writeObjectAsString(john);
-
-        assertNotNull(ser);
-        assertTrue(ser.contains("\"name\":\"John\""));
-        assertTrue(ser.contains("\"marriedTo\":\"/\""));
-        assertTrue(ser.contains("\"name\":\"Marry\""));
+        assertEquals("{\"kids\":[],\"marriedTo\":{\"kids\":[],\"marriedTo\":\"/\",\"name\":\"Marry\"},\"name\":\"John\"}", ser);
 
         // and now de-serialise it back
         Person john2 = mapper.readObject(ser, Person.class);
@@ -164,7 +165,12 @@ public class CircularObjectsTest {
         sue.setFather(karl);
         sue.setMother(andrea);
 
-        Mapper mapper = new MapperBuilder().setAccessModeName("field").setDeduplicateObjects(true).build();
+        Mapper mapper = new MapperBuilder()
+                .setAccessModeName("field")
+                .setDeduplicateObjects(true)
+                .setAttributeOrder(Comparator.naturalOrder())
+                .setPretty(true)
+                .build();
 
         // test deep array
         Person[] people = new Person[4];
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
index 1bcc7f6..15df6d7 100644
--- 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
@@ -26,7 +26,7 @@ public class JsonPointerTrackerTest {
 
     @Test
     public void testJsonPointerTracker() {
-        JsonPointerTracker jptRoot = new JsonPointerTracker(null, "/");
+        JsonPointerTracker jptRoot = JsonPointerTracker.ROOT;
 
         Assert.assertEquals("/", jptRoot.toString());
 
diff --git a/pom.xml b/pom.xml
index b5f776f..4699bc6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -49,7 +49,7 @@
     <checkstyle.version>2.15</checkstyle.version> <!-- checkstyle > 2.15 version do not support java 6 -->
     <!-- JVM values for surefire plugin -->
     <surefire.jvm.params>-Xms1024m -Xmx2048m -Dfile.encoding=UTF-8</surefire.jvm.params>
-    <owb.version>2.0.20</owb.version>
+    <owb.version>2.0.23</owb.version>
   </properties>
 
   <modules>