[JOHNZON-317] lazy lookup of implicit converters
diff --git a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JohnzonBuilder.java b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JohnzonBuilder.java
index 26f380b..5bbfa2c 100644
--- a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JohnzonBuilder.java
+++ b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JohnzonBuilder.java
@@ -18,72 +18,6 @@
  */
 package org.apache.johnzon.jsonb;
 
-import static java.time.format.DateTimeFormatter.ofPattern;
-import static java.time.temporal.ChronoField.DAY_OF_MONTH;
-import static java.time.temporal.ChronoField.HOUR_OF_DAY;
-import static java.time.temporal.ChronoField.MILLI_OF_SECOND;
-import static java.time.temporal.ChronoField.MINUTE_OF_HOUR;
-import static java.time.temporal.ChronoField.MONTH_OF_YEAR;
-import static java.time.temporal.ChronoField.SECOND_OF_MINUTE;
-import static java.time.temporal.ChronoField.YEAR;
-import static java.util.Collections.emptyMap;
-import static java.util.Optional.ofNullable;
-import static javax.json.bind.config.PropertyNamingStrategy.IDENTITY;
-import static javax.json.bind.config.PropertyOrderStrategy.ANY;
-
-import java.io.Closeable;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.ParameterizedType;
-import java.lang.reflect.Type;
-import java.nio.charset.StandardCharsets;
-import java.time.Duration;
-import java.time.Instant;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.time.LocalTime;
-import java.time.OffsetDateTime;
-import java.time.OffsetTime;
-import java.time.Period;
-import java.time.ZoneId;
-import java.time.ZoneOffset;
-import java.time.ZonedDateTime;
-import java.time.format.DateTimeFormatter;
-import java.time.format.DateTimeParseException;
-import java.time.temporal.TemporalAccessor;
-import java.time.temporal.TemporalQueries;
-import java.util.Base64;
-import java.util.Calendar;
-import java.util.Comparator;
-import java.util.Date;
-import java.util.GregorianCalendar;
-import java.util.HashMap;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.SimpleTimeZone;
-import java.util.TimeZone;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Function;
-import java.util.function.Supplier;
-import java.util.stream.Stream;
-
-import javax.json.JsonBuilderFactory;
-import javax.json.bind.Jsonb;
-import javax.json.bind.JsonbBuilder;
-import javax.json.bind.JsonbConfig;
-import javax.json.bind.JsonbException;
-import javax.json.bind.adapter.JsonbAdapter;
-import javax.json.bind.config.BinaryDataStrategy;
-import javax.json.bind.config.PropertyNamingStrategy;
-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.AbstractJsonFactory;
 import org.apache.johnzon.core.JsonGeneratorFactoryImpl;
 import org.apache.johnzon.core.JsonParserFactoryImpl;
@@ -95,7 +29,6 @@
 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.Mapper;
 import org.apache.johnzon.mapper.MapperBuilder;
@@ -103,8 +36,42 @@
 import org.apache.johnzon.mapper.SerializeValueFilter;
 import org.apache.johnzon.mapper.access.AccessMode;
 import org.apache.johnzon.mapper.access.FieldAndMethodAccessMode;
+import org.apache.johnzon.mapper.converter.LocaleConverter;
 import org.apache.johnzon.mapper.internal.AdapterKey;
-import org.apache.johnzon.mapper.internal.ConverterAdapter;
+
+import javax.json.JsonBuilderFactory;
+import javax.json.bind.Jsonb;
+import javax.json.bind.JsonbBuilder;
+import javax.json.bind.JsonbConfig;
+import javax.json.bind.adapter.JsonbAdapter;
+import javax.json.bind.config.BinaryDataStrategy;
+import javax.json.bind.config.PropertyNamingStrategy;
+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.lang.reflect.InvocationTargetException;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+
+import static java.time.format.DateTimeFormatter.ofPattern;
+import static java.util.Collections.emptyMap;
+import static java.util.Optional.ofNullable;
+import static javax.json.bind.config.PropertyNamingStrategy.IDENTITY;
+import static javax.json.bind.config.PropertyOrderStrategy.ANY;
 
 public class JohnzonBuilder implements JsonbBuilder {
     private static final Object NO_BM = new Object();
@@ -212,8 +179,13 @@
         builder.setUseJsRange(toBool( // https://github.com/eclipse-ee4j/jsonb-api/issues/180
                 System.getProperty("johnzon.use-js-range", config.getProperty("johnzon.use-js-range")
                 .map(String::valueOf).orElse("false"))));
-
-        final Map<AdapterKey, Adapter<?, ?>> defaultConverters = createJava8Converters(builder);
+        builder.setUseShortISO8601Format(false);
+        config.getProperty(JsonbConfig.DATE_FORMAT)
+                .map(String.class::cast)
+                .ifPresent(dateFormat -> builder.setAdaptersDateTimeFormatter(config.getProperty(JsonbConfig.LOCALE)
+                        .map(it -> String.class.isInstance(it) ? new LocaleConverter().to(it.toString()) : Locale.class.cast(it))
+                        .map(value -> ofPattern(dateFormat, value))
+                        .orElseGet(() -> ofPattern(dateFormat))));
 
         final JohnzonAdapterFactory factory = config.getProperty("johnzon.factory").map(val -> {
             if (JohnzonAdapterFactory.class.isInstance(val)) {
@@ -241,7 +213,7 @@
                 .orElseGet(() -> new JsonbAccessMode(
                         propertyNamingStrategy, orderValue, visibilityStrategy,
                         !namingStrategyValue.orElse("").equals(PropertyNamingStrategy.CASE_INSENSITIVE),
-                        defaultConverters,
+                        builder.getAdapters(),
                         factory, jsonp, builderFactorySupplier, parserFactoryProvider,
                         config.getProperty("johnzon.accessModeDelegate")
                                 .map(this::toAccessMode)
@@ -266,7 +238,7 @@
             final Type[] args = pt.getActualTypeArguments();
             final JohnzonJsonbAdapter johnzonJsonbAdapter = new JohnzonJsonbAdapter(adapter, args[0], args[1]);
             builder.addAdapter(args[0], args[1], johnzonJsonbAdapter);
-            defaultConverters.put(new AdapterKey(args[0], args[1]), johnzonJsonbAdapter);
+            builder.getAdapters().put(new AdapterKey(args[0], args[1]), johnzonJsonbAdapter);
         }));
 
         ofNullable(config.getProperty("johnzon.fail-on-unknown-properties")
@@ -448,405 +420,6 @@
         return ofNullable(Thread.currentThread().getContextClassLoader()).orElseGet(ClassLoader::getSystemClassLoader);
     }
 
-    // TODO: move these converters in converter package
-    private Map<AdapterKey, Adapter<?, ?>> createJava8Converters(final MapperBuilder builder) {
-        final Map<AdapterKey, Adapter<?, ?>> converters = new HashMap<>();
-
-        final ZoneId zoneIDUTC = ZoneId.of("UTC");
-
-        // built-in converters not in mapper
-        converters.put(new AdapterKey(Period.class, String.class), new ConverterAdapter<>(new Converter<Period>() {
-            @Override
-            public String toString(final Period instance) {
-                return instance.toString();
-            }
-
-            @Override
-            public Period fromString(final String text) {
-                return Period.parse(text);
-            }
-        }));
-        converters.put(new AdapterKey(Duration.class, String.class), new ConverterAdapter<>(new Converter<Duration>() {
-            @Override
-            public String toString(final Duration instance) {
-                return instance.toString();
-            }
-
-            @Override
-            public Duration fromString(final String text) {
-                return Duration.parse(text);
-            }
-        }));
-        converters.put(new AdapterKey(Date.class, String.class), new ConverterAdapter<>(new Converter<Date>() {
-            @Override
-            public String toString(final Date instance) {
-                return ZonedDateTime.ofInstant(instance.toInstant(), zoneIDUTC)
-                        .format(DateTimeFormatter.ISO_ZONED_DATE_TIME);
-            }
-
-            @Override
-            public Date fromString(final String text) {
-                try {
-                    return Date.from(ZonedDateTime.parse(text).toInstant());
-                } catch (final DateTimeParseException dte) {
-                    return Date.from(LocalDateTime.parse(text).toInstant(ZoneOffset.UTC));
-                }
-            }
-        }));
-        converters.put(new AdapterKey(Calendar.class, String.class), new ConverterAdapter<>(new Converter<Calendar>() {
-            @Override
-            public String toString(final Calendar instance) {
-                return toStringCalendar(instance);
-            }
-
-            @Override
-            public Calendar fromString(final String text) {
-                return fromCalendar(text, zdt -> {
-                    final Calendar instance = Calendar.getInstance();
-                    instance.clear();
-                    instance.setTimeZone(TimeZone.getTimeZone(zdt.getZone()));
-                    instance.setTimeInMillis(zdt.toInstant().toEpochMilli());
-                    return instance;
-                });
-            }
-        }));
-        converters.put(new AdapterKey(GregorianCalendar.class, String.class), new ConverterAdapter<>(new Converter<GregorianCalendar>() {
-            @Override
-            public String toString(final GregorianCalendar instance) {
-                return toStringCalendar(instance);
-            }
-
-            @Override
-            public GregorianCalendar fromString(final String text) {
-                return fromCalendar(text, GregorianCalendar::from);
-            }
-        }));
-        converters.put(new AdapterKey(TimeZone.class, String.class), new ConverterAdapter<>(new Converter<TimeZone>() {
-            @Override
-            public String toString(final TimeZone instance) {
-                return instance.getID();
-            }
-
-            @Override
-            public TimeZone fromString(final String text) {
-                checkForDeprecatedTimeZone(text);
-                return TimeZone.getTimeZone(text);
-            }
-        }));
-        converters.put(new AdapterKey(ZoneId.class, String.class), new ConverterAdapter<>(new Converter<ZoneId>() {
-            @Override
-            public String toString(final ZoneId instance) {
-                return instance.getId();
-            }
-
-            @Override
-            public ZoneId fromString(final String text) {
-                return ZoneId.of(text);
-            }
-        }));
-        converters.put(new AdapterKey(ZoneOffset.class, String.class), new ConverterAdapter<>(new Converter<ZoneOffset>() {
-            @Override
-            public String toString(final ZoneOffset instance) {
-                return instance.getId();
-            }
-
-            @Override
-            public ZoneOffset fromString(final String text) {
-                return ZoneOffset.of(text);
-            }
-        }));
-        converters.put(new AdapterKey(SimpleTimeZone.class, String.class), new ConverterAdapter<>(new Converter<SimpleTimeZone>() {
-            @Override
-            public String toString(final SimpleTimeZone instance) {
-                return instance.getID();
-            }
-
-            @Override
-            public SimpleTimeZone fromString(final String text) {
-                checkForDeprecatedTimeZone(text);
-                final TimeZone timeZone = TimeZone.getTimeZone(text);
-                return new SimpleTimeZone(timeZone.getRawOffset(), timeZone.getID());
-            }
-        }));
-        converters.put(new AdapterKey(Instant.class, String.class), new ConverterAdapter<>(new Converter<Instant>() {
-            @Override
-            public String toString(final Instant instance) {
-                return instance.toString();
-            }
-
-            @Override
-            public Instant fromString(final String text) {
-                return Instant.parse(text);
-            }
-        }));
-        converters.put(new AdapterKey(LocalDate.class, String.class), new ConverterAdapter<>(new Converter<LocalDate>() {
-            @Override
-            public String toString(final LocalDate instance) {
-                return instance.toString();
-            }
-
-            @Override
-            public LocalDate fromString(final String text) {
-                return LocalDate.parse(text);
-            }
-        }));
-        converters.put(new AdapterKey(LocalTime.class, String.class), new ConverterAdapter<>(new Converter<LocalTime>() {
-            @Override
-            public String toString(final LocalTime instance) {
-                return instance.toString();
-            }
-
-            @Override
-            public LocalTime fromString(final String text) {
-                return LocalTime.parse(text);
-            }
-        }));
-        converters.put(new AdapterKey(LocalDateTime.class, String.class), new ConverterAdapter<>(new Converter<LocalDateTime>() {
-            @Override
-            public String toString(final LocalDateTime instance) {
-                return instance.toString();
-            }
-
-            @Override
-            public LocalDateTime fromString(final String text) {
-                return LocalDateTime.parse(text);
-            }
-        }));
-        converters.put(new AdapterKey(ZonedDateTime.class, String.class), new ConverterAdapter<>(new Converter<ZonedDateTime>() {
-            @Override
-            public String toString(final ZonedDateTime instance) {
-                return instance.toString();
-            }
-
-            @Override
-            public ZonedDateTime fromString(final String text) {
-                return ZonedDateTime.parse(text);
-            }
-        }));
-        converters.put(new AdapterKey(OffsetDateTime.class, String.class), new ConverterAdapter<>(new Converter<OffsetDateTime>() {
-            @Override
-            public String toString(final OffsetDateTime instance) {
-                return instance.toString();
-            }
-
-            @Override
-            public OffsetDateTime fromString(final String text) {
-                return OffsetDateTime.parse(text);
-            }
-        }));
-        converters.put(new AdapterKey(OffsetTime.class, String.class), new ConverterAdapter<>(new Converter<OffsetTime>() {
-            @Override
-            public String toString(final OffsetTime instance) {
-                return instance.toString();
-            }
-
-            @Override
-            public OffsetTime fromString(final String text) {
-                return OffsetTime.parse(text);
-            }
-        }));
-        addDateFormatConfigConverters(converters, zoneIDUTC);
-
-        converters.forEach((k, v) -> builder.addAdapter(k.getFrom(), k.getTo(), v));
-        return converters;
-    }
-
-    private String toStringCalendar(final Calendar instance) {
-        if (!hasTime(instance)) { // spec
-            final LocalDate localDate = LocalDate.of(
-                    instance.get(Calendar.YEAR),
-                    instance.get(Calendar.MONTH) + 1,
-                    instance.get(Calendar.DAY_OF_MONTH));
-            return localDate.toString() +
-                    (instance.getTimeZone() != null ?
-                            instance.getTimeZone().toZoneId().getRules()
-                                    .getOffset(Instant.ofEpochMilli(TimeUnit.DAYS.toMillis(localDate.toEpochDay()))) : "");
-        }
-        return ZonedDateTime.ofInstant(instance.toInstant(), instance.getTimeZone().toZoneId())
-                .format(DateTimeFormatter.ISO_DATE_TIME);
-    }
-
-    private boolean hasTime(final Calendar instance) {
-        if (!instance.isSet(Calendar.HOUR_OF_DAY)) {
-            return false;
-        }
-        return instance.get(Calendar.HOUR_OF_DAY) != 0 ||
-            (instance.isSet(Calendar.MINUTE)&& instance.get(Calendar.MINUTE) != 0) ||
-            (instance.isSet(Calendar.SECOND) && instance.get(Calendar.SECOND) != 0);
-    }
-
-    private <T extends Calendar> T fromCalendar(final String text, final Function<ZonedDateTime, T> calendarSupplier) {
-        switch (text.length()) {
-            case 10: {
-                final ZonedDateTime date = LocalDate.parse(text)
-                        .atTime(0, 0, 0)
-                        .atZone(ZoneId.of("UTC"));
-                return calendarSupplier.apply(date);
-            }
-            default:
-                final ZonedDateTime zonedDateTime = ZonedDateTime.parse(text);
-                return calendarSupplier.apply(zonedDateTime);
-        }
-    }
-
-
-    private void addDateFormatConfigConverters(final Map<AdapterKey, Adapter<?, ?>> converters, final ZoneId zoneIDUTC) {
-        // config, override defaults
-        config.getProperty(JsonbConfig.DATE_FORMAT).map(String.class::cast).ifPresent(dateFormat -> {
-            final Optional<Locale> locale = config.getProperty(JsonbConfig.LOCALE).map(Locale.class::cast);
-            final DateTimeFormatter formatter = locale.isPresent() ? ofPattern(dateFormat, locale.get()) : ofPattern(dateFormat);
-
-            converters.put(new AdapterKey(Date.class, String.class), new ConverterAdapter<>(new Converter<Date>() {
-
-                @Override
-                public String toString(final Date instance) {
-                    return formatter.format(ZonedDateTime.ofInstant(instance.toInstant(), zoneIDUTC));
-                }
-
-                @Override
-                public Date fromString(final String text) {
-                    try {
-                        return Date.from(parseZonedDateTime(text, formatter, zoneIDUTC).toInstant());
-                    } catch (final DateTimeParseException dpe) {
-                        return Date.from(LocalDateTime.parse(text).toInstant(ZoneOffset.UTC));
-                    }
-                }
-            }));
-            converters.put(new AdapterKey(LocalDateTime.class, String.class), new ConverterAdapter<>(new Converter<LocalDateTime>() {
-
-                @Override
-                public String toString(final LocalDateTime instance) {
-                    return formatter.format(ZonedDateTime.ofInstant(instance.toInstant(ZoneOffset.UTC), zoneIDUTC));
-                }
-
-                @Override
-                public LocalDateTime fromString(final String text) {
-                    try {
-                        return parseZonedDateTime(text, formatter, zoneIDUTC).toLocalDateTime();
-                    } catch (final DateTimeParseException dpe) {
-                        return LocalDateTime.parse(text);
-                    }
-                }
-            }));
-            converters.put(new AdapterKey(LocalDate.class, String.class), new ConverterAdapter<>(new Converter<LocalDate>() {
-
-                @Override
-                public String toString(final LocalDate instance) {
-                    return formatter.format(ZonedDateTime.ofInstant(Instant.ofEpochMilli(TimeUnit.DAYS.toMillis(instance.toEpochDay())), zoneIDUTC));
-                }
-
-                @Override
-                public LocalDate fromString(final String text) {
-                    try {
-                        return parseZonedDateTime(text, formatter, zoneIDUTC).toLocalDate();
-                    } catch (final DateTimeParseException dpe) {
-                        return LocalDate.parse(text);
-                    }
-                }
-            }));
-            converters.put(new AdapterKey(OffsetDateTime.class, String.class), new ConverterAdapter<>(new Converter<OffsetDateTime>() {
-
-                @Override
-                public String toString(final OffsetDateTime instance) {
-                    return formatter.format(ZonedDateTime.ofInstant(instance.toInstant(), zoneIDUTC));
-                }
-
-                @Override
-                public OffsetDateTime fromString(final String text) {
-                    try {
-                        return parseZonedDateTime(text, formatter, zoneIDUTC).toOffsetDateTime();
-                    } catch (final DateTimeParseException dpe) {
-                        return OffsetDateTime.parse(text);
-                    }
-                }
-            }));
-            converters.put(new AdapterKey(ZonedDateTime.class, String.class), new ConverterAdapter<>(new Converter<ZonedDateTime>() {
-
-                @Override
-                public String toString(final ZonedDateTime instance) {
-                    return formatter.format(ZonedDateTime.ofInstant(instance.toInstant(), zoneIDUTC));
-                }
-
-                @Override
-                public ZonedDateTime fromString(final String text) {
-                    try {
-                        return parseZonedDateTime(text, formatter, zoneIDUTC);
-                    } catch (final DateTimeParseException dpe) {
-                        return ZonedDateTime.parse(text);
-                    }
-                }
-            }));
-            converters.put(new AdapterKey(Calendar.class, String.class), new ConverterAdapter<>(new Converter<Calendar>() {
-
-                @Override
-                public String toString(final Calendar instance) {
-                    return formatter.format(ZonedDateTime.ofInstant(instance.toInstant(), instance.getTimeZone().toZoneId()));
-                }
-
-                @Override
-                public Calendar fromString(final String text) {
-                    final ZonedDateTime zonedDateTime = parseZonedDateTime(text, formatter, zoneIDUTC);
-                    final Calendar instance = Calendar.getInstance();
-                    instance.setTimeZone(TimeZone.getTimeZone(zonedDateTime.getZone()));
-                    instance.setTime(Date.from(zonedDateTime.toInstant()));
-                    return instance;
-                }
-            }));
-            converters.put(new AdapterKey(GregorianCalendar.class, String.class), new ConverterAdapter<>(new Converter<GregorianCalendar>() {
-
-                @Override
-                public String toString(final GregorianCalendar instance) {
-                    return formatter.format(ZonedDateTime.ofInstant(instance.toInstant(), instance.getTimeZone().toZoneId()));
-                }
-
-                @Override
-                public GregorianCalendar fromString(final String text) {
-                    final ZonedDateTime zonedDateTime = parseZonedDateTime(text, formatter, zoneIDUTC);
-                    final Calendar instance = GregorianCalendar.getInstance();
-                    instance.setTimeZone(TimeZone.getTimeZone(zonedDateTime.getZone()));
-                    instance.setTime(Date.from(zonedDateTime.toInstant()));
-                    return GregorianCalendar.class.cast(instance);
-                }
-            }));
-            converters.put(new AdapterKey(Instant.class, String.class), new ConverterAdapter<>(new Converter<Instant>() {
-
-                @Override
-                public String toString(final Instant instance) {
-                    return formatter.format(ZonedDateTime.ofInstant(instance, zoneIDUTC));
-                }
-
-                @Override
-                public Instant fromString(final String text) {
-                    return parseZonedDateTime(text, formatter, zoneIDUTC).toInstant();
-                }
-            }));
-        });
-    }
-    
-    private static ZonedDateTime parseZonedDateTime(final String text, final DateTimeFormatter formatter, final ZoneId defaultZone){
-        TemporalAccessor parse = formatter.parse(text);
-        ZoneId zone = parse.query(TemporalQueries.zone());
-        if (Objects.isNull(zone)) {
-            zone = defaultZone;
-        }
-        int year = parse.isSupported(YEAR) ? parse.get(YEAR) : 0;
-        int month = parse.isSupported(MONTH_OF_YEAR) ? parse.get(MONTH_OF_YEAR) : 0;
-        int day = parse.isSupported(DAY_OF_MONTH) ? parse.get(DAY_OF_MONTH) : 0;
-        int hour = parse.isSupported(HOUR_OF_DAY) ? parse.get(HOUR_OF_DAY) : 0;
-        int minute = parse.isSupported(MINUTE_OF_HOUR) ? parse.get(MINUTE_OF_HOUR) : 0;
-        int second = parse.isSupported(SECOND_OF_MINUTE) ? parse.get(SECOND_OF_MINUTE) : 0;
-        int millisecond = parse.isSupported(MILLI_OF_SECOND) ? parse.get(MILLI_OF_SECOND) : 0;
-        return ZonedDateTime.of(year, month, day, hour, minute, second, millisecond, zone);
-    }
-
-    private static void checkForDeprecatedTimeZone(final String text) {
-        switch (text) {
-            case "CST": // really for TCK, this sucks for end users so we don't fail for all deprecated zones
-                throw new JsonbException("Deprecated timezone: '" + text + '"');
-            default:
-        }
-    }
-
     private Map<String, ?> generatorConfig() {
         final Map<String, Object> map = new HashMap<>();
         if (config == null) {
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 1130f67..363f9b6 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
@@ -441,20 +441,20 @@
             converter = new JohnzonJsonbAdapter(instance.getValue(), actualTypeArguments[0], actualTypeArguments[1]);
         } else if (dateFormat != null) { // TODO: support lists, LocalDate?
             if (Date.class == type) {
-                converter = new ConverterAdapter<>(new JsonbDateConverter(dateFormat));
+                converter = new ConverterAdapter<>(new JsonbDateConverter(dateFormat), Date.class);
             } else if (LocalDateTime.class == type) {
-                converter = new ConverterAdapter<>(new JsonbLocalDateTimeConverter(dateFormat));
+                converter = new ConverterAdapter<>(new JsonbLocalDateTimeConverter(dateFormat), LocalDateTime.class);
             } else if (LocalDate.class == type) {
-                converter = new ConverterAdapter<>(new JsonbLocalDateConverter(dateFormat));
+                converter = new ConverterAdapter<>(new JsonbLocalDateConverter(dateFormat), LocalDate.class);
             } else if (ZonedDateTime.class == type) {
-                converter = new ConverterAdapter<>(new JsonbZonedDateTimeConverter(dateFormat));
+                converter = new ConverterAdapter<>(new JsonbZonedDateTimeConverter(dateFormat), ZonedDateTime.class);
             } else { // can happen if set on the class, todo: refine the checks
                 converter = null; // todo: should we fallback on numberformat?
             }
         } else if (numberFormat != null) {  // TODO: support lists?
-            converter = new ConverterAdapter<>(new JsonbNumberConverter(numberFormat));
+            converter = new ConverterAdapter<>(new JsonbNumberConverter(numberFormat), Number.class);
         } else {
-            converter = new ConverterAdapter<>(new JsonbValueConverter());
+            converter = new ConverterAdapter<>(new JsonbValueConverter(), Object.class);
         }
         return converter;
     }
@@ -985,7 +985,7 @@
                 try {
                     MapperConverter mapperConverter = johnzonConverter.value().newInstance();
                     if (mapperConverter instanceof Converter) {
-                        converter = new ConverterAdapter<>((Converter) mapperConverter);
+                        converter = new ConverterAdapter<>((Converter) mapperConverter, annotationHolder.getType());
                     } else if (mapperConverter instanceof ObjectConverter.Reader) {
                         reader = (ObjectConverter.Reader) mapperConverter;
                     }
@@ -1045,7 +1045,7 @@
                 try {
                     MapperConverter mapperConverter = johnzonConverter.value().newInstance();
                     if (mapperConverter instanceof Converter) {
-                        converter = new ConverterAdapter<>((Converter) mapperConverter) ;
+                        converter = new ConverterAdapter<>((Converter) mapperConverter, reader.getType()) ;
                     } else if (mapperConverter instanceof ObjectConverter.Writer) {
                         writer = (ObjectConverter.Writer) mapperConverter;
                     }
diff --git a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/converter/JohnzonJsonbAdapter.java b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/converter/JohnzonJsonbAdapter.java
index 7fc505f..079c782 100644
--- a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/converter/JohnzonJsonbAdapter.java
+++ b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/converter/JohnzonJsonbAdapter.java
@@ -19,6 +19,7 @@
 package org.apache.johnzon.jsonb.converter;
 
 import org.apache.johnzon.mapper.TypeAwareAdapter;
+import org.apache.johnzon.mapper.internal.AdapterKey;
 
 import javax.json.bind.JsonbException;
 import javax.json.bind.adapter.JsonbAdapter;
@@ -28,11 +29,13 @@
     private final JsonbAdapter<OriginalType, JsonType> delegate;
     private final Type from;
     private final Type to;
+    private final AdapterKey key;
 
     public JohnzonJsonbAdapter(final JsonbAdapter<OriginalType, JsonType> delegate, final Type from, final Type to) {
         this.delegate = delegate;
         this.from = from;
         this.to = to;
+        this.key = new AdapterKey(from, to);
     }
 
     @Override
@@ -60,6 +63,11 @@
     }
 
     @Override
+    public AdapterKey getKey() {
+        return key;
+    }
+
+    @Override
     public Type getTo() {
         return to;
     }
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/JohnzonJsonbTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/JohnzonJsonbTest.java
index debf27f..757359b 100644
--- a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/JohnzonJsonbTest.java
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/JohnzonJsonbTest.java
@@ -24,9 +24,14 @@
 import javax.json.bind.Jsonb;
 import javax.json.bind.JsonbBuilder;
 
+import org.apache.johnzon.jsonb.test.JsonbRule;
+import org.junit.Rule;
 import org.junit.Test;
 
 public class JohnzonJsonbTest {
+    @Rule
+    public final JsonbRule rule = new JsonbRule();
+
     @Test
     public void jsonArray() throws Exception {
         try (final Jsonb jsonb = JsonbBuilder.create()) {
@@ -35,4 +40,27 @@
             assertEquals(json, array.toString());
         }
     }
+
+    @Test
+    public void longBounds() {
+        final String max = rule.toJson(new LongWrapper(Long.MAX_VALUE));
+        assertEquals("{\"value\":9223372036854775807}", max);
+        assertEquals(Long.MAX_VALUE, rule.fromJson(max, LongWrapper.class).value, 0);
+
+        final String min = rule.toJson(new LongWrapper(Long.MIN_VALUE));
+        assertEquals("{\"value\":-9223372036854775808}", min);
+        assertEquals(Long.MIN_VALUE, rule.fromJson(min, LongWrapper.class).value, 0);
+    }
+
+    public static class LongWrapper {
+        public Long value;
+
+        public LongWrapper() {
+            // no-op
+        }
+
+        public LongWrapper(final Long value) {
+            this.value = value;
+        }
+    }
 }
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperBuilder.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperBuilder.java
index 81cc049..ade0e26 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperBuilder.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperBuilder.java
@@ -28,25 +28,18 @@
 import org.apache.johnzon.mapper.access.FieldAccessMode;
 import org.apache.johnzon.mapper.access.FieldAndMethodAccessMode;
 import org.apache.johnzon.mapper.access.MethodAccessMode;
-import org.apache.johnzon.mapper.converter.BigDecimalConverter;
-import org.apache.johnzon.mapper.converter.BigIntegerConverter;
 import org.apache.johnzon.mapper.converter.BooleanConverter;
 import org.apache.johnzon.mapper.converter.ByteConverter;
 import org.apache.johnzon.mapper.converter.CachedDelegateConverter;
 import org.apache.johnzon.mapper.converter.CharacterConverter;
-import org.apache.johnzon.mapper.converter.ClassConverter;
-import org.apache.johnzon.mapper.converter.DateConverter;
 import org.apache.johnzon.mapper.converter.DoubleConverter;
 import org.apache.johnzon.mapper.converter.FloatConverter;
 import org.apache.johnzon.mapper.converter.IntegerConverter;
-import org.apache.johnzon.mapper.converter.LocaleConverter;
 import org.apache.johnzon.mapper.converter.LongConverter;
 import org.apache.johnzon.mapper.converter.ShortConverter;
-import org.apache.johnzon.mapper.converter.StringConverter;
-import org.apache.johnzon.mapper.converter.URIConverter;
-import org.apache.johnzon.mapper.converter.URLConverter;
 import org.apache.johnzon.mapper.internal.AdapterKey;
 import org.apache.johnzon.mapper.internal.ConverterAdapter;
+import org.apache.johnzon.mapper.map.LazyConverterMap;
 
 import javax.json.JsonBuilderFactory;
 import javax.json.JsonReaderFactory;
@@ -56,58 +49,20 @@
 import java.io.Closeable;
 import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
-import java.math.BigDecimal;
-import java.math.BigInteger;
-import java.net.URI;
-import java.net.URL;
 import java.nio.charset.Charset;
+import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Comparator;
-import java.util.Date;
 import java.util.HashMap;
-import java.util.Locale;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
 // this class is responsible to hold any needed config
 // to build the runtime
 public class MapperBuilder {
-    private static final Map<AdapterKey, Adapter<?, ?>> DEFAULT_CONVERTERS = new HashMap<AdapterKey, Adapter<?, ?>>(24);
-
-    static {
-        //DEFAULT_CONVERTERS.put(Date.class, new DateConverter("yyyy-MM-dd'T'HH:mm:ssZ")); // ISO8601 long RFC822 zone
-        DEFAULT_CONVERTERS.put(new AdapterKey(Date.class, String.class), new ConverterAdapter<Date>(new DateConverter("yyyyMMddHHmmssZ"))); // ISO8601 short
-        DEFAULT_CONVERTERS.put(new AdapterKey(URL.class, String.class), new ConverterAdapter<URL>(new URLConverter()));
-        DEFAULT_CONVERTERS.put(new AdapterKey(URI.class, String.class), new ConverterAdapter<URI>(new URIConverter()));
-        DEFAULT_CONVERTERS.put(new AdapterKey(Class.class, String.class), new ConverterAdapter<Class<?>>(new ClassConverter()));
-        DEFAULT_CONVERTERS.put(new AdapterKey(String.class, String.class), new ConverterAdapter<String>(new StringConverter()));
-        DEFAULT_CONVERTERS.put(new AdapterKey(BigDecimal.class, String.class), new ConverterAdapter<BigDecimal>(new BigDecimalConverter()));
-        DEFAULT_CONVERTERS.put(new AdapterKey(BigInteger.class, String.class), new ConverterAdapter<BigInteger>(new BigIntegerConverter()));
-        /* primitives should be hanlded low level and adapters will wrap them in string which is unlikely
-        DEFAULT_CONVERTERS.put(new AdapterKey(Byte.class, String.class), new ConverterAdapter<Byte>(new CachedDelegateConverter<Byte>(new ByteConverter())));
-        DEFAULT_CONVERTERS.put(new AdapterKey(Character.class, String.class), new ConverterAdapter<Character>(new CharacterConverter()));
-        DEFAULT_CONVERTERS.put(new AdapterKey(Double.class, String.class), new ConverterAdapter<Double>(new DoubleConverter()));
-        DEFAULT_CONVERTERS.put(new AdapterKey(Float.class, String.class), new ConverterAdapter<Float>(new FloatConverter()));
-        DEFAULT_CONVERTERS.put(new AdapterKey(Integer.class, String.class), new ConverterAdapter<Integer>(new IntegerConverter()));
-        DEFAULT_CONVERTERS.put(new AdapterKey(Long.class, String.class), new ConverterAdapter<Long>(new LongConverter()));
-        DEFAULT_CONVERTERS.put(new AdapterKey(Short.class, String.class), new ConverterAdapter<Short>(new ShortConverter()));
-        DEFAULT_CONVERTERS.put(new AdapterKey(Boolean.class, String.class), new ConverterAdapter<Boolean>(new CachedDelegateConverter<Boolean>(new BooleanConverter())));
-        DEFAULT_CONVERTERS.put(new AdapterKey(byte.class, String.class), DEFAULT_CONVERTERS.get(new AdapterKey(Byte.class, String.class)));
-        DEFAULT_CONVERTERS.put(new AdapterKey(char.class, String.class), DEFAULT_CONVERTERS.get(new AdapterKey(Character.class, String.class)));
-        DEFAULT_CONVERTERS.put(new AdapterKey(double.class, String.class), DEFAULT_CONVERTERS.get(new AdapterKey(Double.class, String.class)));
-        DEFAULT_CONVERTERS.put(new AdapterKey(float.class, String.class), DEFAULT_CONVERTERS.get(new AdapterKey(Float.class, String.class)));
-        DEFAULT_CONVERTERS.put(new AdapterKey(int.class, String.class), DEFAULT_CONVERTERS.get(new AdapterKey(Integer.class, String.class)));
-        DEFAULT_CONVERTERS.put(new AdapterKey(long.class, String.class), DEFAULT_CONVERTERS.get(new AdapterKey(Long.class, String.class)));
-        DEFAULT_CONVERTERS.put(new AdapterKey(short.class, String.class), DEFAULT_CONVERTERS.get(new AdapterKey(Short.class, String.class)));
-        DEFAULT_CONVERTERS.put(new AdapterKey(boolean.class, String.class), DEFAULT_CONVERTERS.get(new AdapterKey(Boolean.class, String.class)));
-        */
-        DEFAULT_CONVERTERS.put(new AdapterKey(Locale.class, String.class), new LocaleConverter());
-    }
-
     private JsonReaderFactory readerFactory;
     private JsonGeneratorFactory generatorFactory;
     private JsonProvider provider;
@@ -134,7 +89,7 @@
     private boolean enforceQuoteString;
     private AccessMode accessMode;
     private Charset encoding = Charset.forName(System.getProperty("johnzon.mapper.encoding", "UTF-8"));
-    private ConcurrentMap<AdapterKey, Adapter<?, ?>> adapters = new ConcurrentHashMap<AdapterKey, Adapter<?, ?>>(DEFAULT_CONVERTERS);
+    private LazyConverterMap adapters = new LazyConverterMap();
     private Map<Class<?>, ObjectConverter.Reader<?>> objectConverterReaders = new HashMap<Class<?>, ObjectConverter.Reader<?>>();
     private Map<Class<?>, ObjectConverter.Writer<?>> objectConverterWriters = new HashMap<Class<?>, ObjectConverter.Writer<?>>();
     private Map<Class<?>, String[]> ignoredForFields = new HashMap<Class<?>, String[]>();
@@ -244,14 +199,14 @@
         }
 
         if (primitiveConverters) {
-            adapters.put(new AdapterKey(Byte.class, String.class), new ConverterAdapter<Byte>(new CachedDelegateConverter<Byte>(new ByteConverter())));
-            adapters.put(new AdapterKey(Character.class, String.class), new ConverterAdapter<Character>(new CharacterConverter()));
-            adapters.put(new AdapterKey(Double.class, String.class), new ConverterAdapter<Double>(new DoubleConverter()));
-            adapters.put(new AdapterKey(Float.class, String.class), new ConverterAdapter<Float>(new FloatConverter()));
-            adapters.put(new AdapterKey(Integer.class, String.class), new ConverterAdapter<Integer>(new IntegerConverter()));
-            adapters.put(new AdapterKey(Long.class, String.class), new ConverterAdapter<Long>(new LongConverter()));
-            adapters.put(new AdapterKey(Short.class, String.class), new ConverterAdapter<Short>(new ShortConverter()));
-            adapters.put(new AdapterKey(Boolean.class, String.class), new ConverterAdapter<Boolean>(new CachedDelegateConverter<Boolean>(new BooleanConverter())));
+            adapters.put(new AdapterKey(Byte.class, String.class), new ConverterAdapter<>(new CachedDelegateConverter<>(new ByteConverter()), Byte.class));
+            adapters.put(new AdapterKey(Character.class, String.class), new ConverterAdapter<>(new CharacterConverter(), Character.class));
+            adapters.put(new AdapterKey(Double.class, String.class), new ConverterAdapter<>(new DoubleConverter(), Double.class));
+            adapters.put(new AdapterKey(Float.class, String.class), new ConverterAdapter<>(new FloatConverter(), Float.class));
+            adapters.put(new AdapterKey(Integer.class, String.class), new ConverterAdapter<>(new IntegerConverter(), Integer.class));
+            adapters.put(new AdapterKey(Long.class, String.class), new ConverterAdapter<>(new LongConverter(), Long.class));
+            adapters.put(new AdapterKey(Short.class, String.class), new ConverterAdapter<>(new ShortConverter(), Short.class));
+            adapters.put(new AdapterKey(Boolean.class, String.class), new ConverterAdapter<>(new CachedDelegateConverter<>(new BooleanConverter()), Boolean.class));
             adapters.put(new AdapterKey(byte.class, String.class), adapters.get(new AdapterKey(Byte.class, String.class)));
             adapters.put(new AdapterKey(char.class, String.class), adapters.get(new AdapterKey(Character.class, String.class)));
             adapters.put(new AdapterKey(double.class, String.class), adapters.get(new AdapterKey(Double.class, String.class)));
@@ -278,6 +233,25 @@
                 closeables);
     }
 
+    public ConcurrentHashMap<AdapterKey, Adapter<?,?>> getAdapters() {
+        return adapters;
+    }
+
+    public MapperBuilder setUseShortISO8601Format(final boolean useShortISO8601Format) {
+        adapters.setUseShortISO8601Format(useShortISO8601Format);
+        return this;
+    }
+
+    public MapperBuilder setAdaptersDateTimeFormatter(final DateTimeFormatter dateTimeFormatter) {
+        adapters.setDateTimeFormatter(dateTimeFormatter);
+        return this;
+    }
+
+    public MapperBuilder setAdaptersDateTimeFormatterString(final String dateTimeFormatter) {
+        adapters.setDateTimeFormatter(DateTimeFormatter.ofPattern(dateTimeFormatter));
+        return this;
+    }
+
     public MapperBuilder setInterfaceImplementationMapping(final Map<Class<?>, Class<?>> interfaceImplementationMapping) {
         this.interfaceImplementationMapping = interfaceImplementationMapping;
         return this;
@@ -395,13 +369,13 @@
 
     @Deprecated // use addAdapter
     public MapperBuilder addPropertyEditor(final Class<?> clazz, final Converter<?> converter) {
-        adapters.put(new AdapterKey(clazz, String.class), new ConverterAdapter(converter));
+        adapters.put(new AdapterKey(clazz, String.class), new ConverterAdapter(converter, clazz));
         return this;
     }
 
     @Deprecated // use addAdapter
     public MapperBuilder addConverter(final Type clazz, final Converter<?> converter) {
-        adapters.put(new AdapterKey(clazz, String.class), new ConverterAdapter(converter));
+        adapters.put(new AdapterKey(clazz, String.class), new ConverterAdapter(converter, clazz));
         return this;
     }
 
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 3046376..d0b17d5 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
@@ -22,6 +22,7 @@
 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.map.LazyConverterMap;
 
 import javax.json.JsonValue;
 import java.lang.reflect.Type;
@@ -67,7 +68,7 @@
     private final boolean supportEnumMapDeserialization; // for tck
     private final AccessMode accessMode;
     private final Charset encoding;
-    private final ConcurrentMap<AdapterKey, Adapter<?, ?>> adapters;
+    private final LazyConverterMap adapters;
     private final ConcurrentMap<Adapter<?, ?>, AdapterKey> reverseAdapters;
 
     private final Map<Class<?>, ObjectConverter.Writer<?>> objectConverterWriters;
@@ -95,7 +96,7 @@
 
     //disable checkstyle for 10+ parameters
     //CHECKSTYLE:OFF
-    public MapperConfig(final ConcurrentMap<AdapterKey, Adapter<?, ?>> adapters,
+    public MapperConfig(final LazyConverterMap adapters,
                         final Map<Class<?>, ObjectConverter.Writer<?>> objectConverterWriters,
                         final Map<Class<?>, ObjectConverter.Reader<?>> objectConverterReaders,
                         final int version, final boolean close,
@@ -218,12 +219,12 @@
         if (Class.class.isInstance(aClass)) {
             final Class<?> clazz = Class.class.cast(aClass);
             if (Enum.class.isAssignableFrom(clazz)) {
-                final Adapter<?, ?> enumConverter = new ConverterAdapter(new EnumConverter(clazz));
+                final Adapter<?, ?> enumConverter = new ConverterAdapter(new EnumConverter(clazz), clazz);
                 adapters.putIfAbsent(new AdapterKey(String.class, aClass), enumConverter);
                 return enumConverter;
             }
         }
-        final List<AdapterKey> matched = adapters.keySet().stream()
+        final List<AdapterKey> matched = adapters.adapterKeys().stream()
                 .filter(k -> k.isAssignableFrom(aClass))
                 .collect(toList());
         if (matched.size() == 1) {
@@ -382,7 +383,7 @@
         return encoding;
     }
 
-    public ConcurrentMap<AdapterKey, Adapter<?, ?>> getAdapters() {
+    public LazyConverterMap getAdapters() {
         return adapters;
     }
 
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 1e90acf..25a5134 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
@@ -503,15 +503,9 @@
             if (JsonObject.class == key.getTo() || JsonStructure.class == key.getTo()) {
                 return converter.to(jsonValue.asJsonObject());
             }
-
-            //X TODO maybe we can put this into MapperConfig?
-            //X      config.getAdapter(AdapterKey)
-            //X      config.getAdapterKey(Adapter)
-            final AdapterKey adapterKey = getAdapterKey(converter);
-
             final Object param;
             try {
-                Type to = adapterKey.getTo();
+                Type to = key.getTo();
                 param = buildObject(to, JsonObject.class.cast(jsonValue), to instanceof Class, jsonPointer);
             } catch (final Exception e) {
                 throw new MapperException(e);
@@ -575,9 +569,7 @@
 
         if (adapterKey == null) {
             if (converter instanceof TypeAwareAdapter) {
-                TypeAwareAdapter typeAwareAdapter = (TypeAwareAdapter) converter;
-                adapterKey = new AdapterKey(typeAwareAdapter.getFrom(), typeAwareAdapter.getTo());
-                config.getReverseAdapters().putIfAbsent(converter, adapterKey);
+                return TypeAwareAdapter.class.cast(converter).getKey();
 
             } else {
                 Class<?> current = converter.getClass();
@@ -655,7 +647,7 @@
             if (JsonObject.class == type || JsonStructure.class == type) {
                 return jsonValue;
             }
-            final boolean typedAdapter = TypeAwareAdapter.class.isInstance(itemConverter);
+            final boolean typedAdapter = !ConverterAdapter.class.isInstance(itemConverter) && TypeAwareAdapter.class.isInstance(itemConverter);
             final Object object = buildObject(
                     baseInstance != null ? baseInstance.getClass() : (
                             typedAdapter ? TypeAwareAdapter.class.cast(itemConverter).getTo() : type),
@@ -1136,12 +1128,12 @@
         if (Class.class.isInstance(aClass)) {
             final Class<?> clazz = Class.class.cast(aClass);
             if (Enum.class.isAssignableFrom(clazz)) {
-                final Adapter<?, ?> enumConverter = new ConverterAdapter(new EnumConverter(clazz));
+                final Adapter<?, ?> enumConverter = new ConverterAdapter(new EnumConverter(clazz), clazz);
                 config.getAdapters().putIfAbsent(new AdapterKey(String.class, aClass), enumConverter);
                 return enumConverter;
             }
         }
-        final List<AdapterKey> matched = config.getAdapters().keySet().stream()
+        final List<AdapterKey> matched = config.getAdapters().adapterKeys().stream()
                 .filter(k -> k.isAssignableFrom(aClass))
                 .collect(toList());
         if (matched.size() == 1) {
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 1c5db42..1c5b045 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
@@ -160,16 +160,27 @@
                 }
                 if (theObjectConverter == null) {
                     Adapter adapter;
+                    final Type readerType = reader.getType();
                     if (converter instanceof Converter) {
-                        adapter = new ConverterAdapter((Converter) converter);
+                        adapter = new ConverterAdapter((Converter) converter, readerType);
                     } else {
                         adapter = (Adapter) converter;
                     }
 
-                    if (matches(reader.getType(), adapter)) {
+                    if (matches(readerType, adapter)) {
                         theConverter = adapter;
                     } else {
-                        theItemConverter = adapter;
+                        if (converter instanceof Converter) {
+                            if (ParameterizedType.class.isInstance(readerType) && ParameterizedType.class.cast(readerType).getActualTypeArguments().length > 0) {
+                                final Type[] args = ParameterizedType.class.cast(readerType).getActualTypeArguments();
+                                // List<A> or Map<String, A> lead to read the last arg in all cases
+                                theItemConverter = new ConverterAdapter((Converter) converter, args[args.length - 1]);
+                            } else {
+                                theItemConverter = adapter;
+                            }
+                        } else {
+                            theItemConverter = adapter;
+                        }
                     }
                 }
             }
@@ -237,18 +248,29 @@
                 if (converter instanceof ObjectConverter.Reader) {
                     theObjectConverter = (ObjectConverter.Reader) converter;
                 }
+                final Type writerType = writer.getType();
                 if (theObjectConverter == null) {
                     Adapter adapter;
                     if (converter instanceof Converter) {
-                        adapter = new ConverterAdapter((Converter) converter);
+                        adapter = new ConverterAdapter((Converter) converter, writerType);
                     } else {
                         adapter = (Adapter) converter;
                     }
 
-                    if (matches(writer.getType(), adapter)) {
+                    if (matches(writerType, adapter)) {
                         theConverter = adapter;
                     } else {
-                        theItemConverter = adapter;
+                        if (converter instanceof Converter) {
+                            if (ParameterizedType.class.isInstance(writerType) && ParameterizedType.class.cast(writerType).getActualTypeArguments().length > 0) {
+                                final Type[] args = ParameterizedType.class.cast(writerType).getActualTypeArguments();
+                                // List<A> or Map<String, A> lead to read the last arg in all cases
+                                theItemConverter = new ConverterAdapter((Converter) converter, args[args.length - 1]);
+                            } else {
+                                theItemConverter = adapter;
+                            }
+                        } else {
+                            theItemConverter = adapter;
+                        }
                     }
                 }
             }
@@ -677,7 +699,7 @@
                 final AdapterKey key = new AdapterKey(String.class, type);
                 converter = adapters.get(key); // first ensure user didnt override it
                 if (converter == null) {
-                    converter = new ConverterAdapter(new EnumConverter(type));
+                    converter = new ConverterAdapter(new EnumConverter(type), type);
                     adapters.put(key, (Adapter<?, ?>) converter);
                 }
             }
@@ -694,12 +716,12 @@
         public MapBuilderReader(final Map<String, Getter> objectGetters, final String[] paths, final int version) {
             this.getters = objectGetters;
             this.paths = paths;
-            this.template = new LinkedHashMap<String, Object>();
+            this.template = new LinkedHashMap<>();
             this.version = version;
 
             Map<String, Object> last = this.template;
             for (int i = 1; i < paths.length; i++) {
-                final Map<String, Object> newLast = new LinkedHashMap<String, Object>();
+                final Map<String, Object> newLast = new LinkedHashMap<>();
                 last.put(paths[i], newLast);
                 last = newLast;
             }
@@ -707,7 +729,7 @@
 
         @Override
         public Object read(final Object instance) {
-            final Map<String, Object> map = new LinkedHashMap<String, Object>(template);
+            final Map<String, Object> map = new LinkedHashMap<>(template);
             Map<String, Object> nested = map;
             for (int i = 1; i < paths.length; i++) {
                 nested = Map.class.cast(nested.get(paths[i]));
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/TypeAwareAdapter.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/TypeAwareAdapter.java
index cff2fc0..b956803 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/TypeAwareAdapter.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/TypeAwareAdapter.java
@@ -18,9 +18,15 @@
  */
 package org.apache.johnzon.mapper;
 
+import org.apache.johnzon.mapper.internal.AdapterKey;
+
 import java.lang.reflect.Type;
 
 public interface TypeAwareAdapter<A, B> extends Adapter<A, B> {
     Type getTo();
     Type getFrom();
+
+    default AdapterKey getKey() {
+        return new AdapterKey(getFrom(), getTo());
+    }
 }
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/BaseAccessMode.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/BaseAccessMode.java
index edbc379..7bd011e 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/BaseAccessMode.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/BaseAccessMode.java
@@ -197,7 +197,7 @@
                         try {
                             MapperConverter mapperConverter = JohnzonConverter.class.cast(a).value().newInstance();
                             if (mapperConverter instanceof Converter) {
-                                final Adapter<?, ?> converter = new ConverterAdapter((Converter) mapperConverter);
+                                final Adapter<?, ?> converter = new ConverterAdapter((Converter) mapperConverter, constructor.getGenericParameterTypes()[i]);
                                 if (matches(constructor.getParameterTypes()[i], converter)) {
                                     constructorParameterConverters[i] = converter;
                                     constructorItemParameterConverters[i] = null;
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/converter/DateWithCopyConverter.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/converter/DateWithCopyConverter.java
index 431f133..85abb26 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/converter/DateWithCopyConverter.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/converter/DateWithCopyConverter.java
@@ -28,7 +28,7 @@
     private final Adapter<Date, String> delegate;
 
     public DateWithCopyConverter(final Adapter<Date, String> delegate) {
-        this.delegate = delegate == null ? new ConverterAdapter<Date>(new DateConverter("yyyyMMddHHmmssZ")) : delegate;
+        this.delegate = delegate == null ? new ConverterAdapter<>(new DateConverter("yyyyMMddHHmmssZ"), Date.class) : delegate;
     }
 
     @Override
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/converter/LocaleConverter.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/converter/LocaleConverter.java
index a03b2a1..6dd330f 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/converter/LocaleConverter.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/converter/LocaleConverter.java
@@ -18,12 +18,31 @@
  */
 package org.apache.johnzon.mapper.converter;
 
-import org.apache.johnzon.mapper.Adapter;
+import org.apache.johnzon.mapper.TypeAwareAdapter;
+import org.apache.johnzon.mapper.internal.AdapterKey;
 
+import java.lang.reflect.Type;
 import java.util.Locale;
 
 // from [lang]
-public class LocaleConverter implements Adapter<Locale, String> {
+public class LocaleConverter implements TypeAwareAdapter<Locale, String> {
+    private final AdapterKey key = new AdapterKey(Locale.class, String.class);
+
+    @Override
+    public Type getTo() {
+        return key.getTo();
+    }
+
+    @Override
+    public Type getFrom() {
+        return key.getFrom();
+    }
+
+    @Override
+    public AdapterKey getKey() {
+        return key;
+    }
+
     @Override
     public String from(final Locale instance) {
         return instance.toString();
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/internal/ConverterAdapter.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/internal/ConverterAdapter.java
index c3bb90e..afea76b 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/internal/ConverterAdapter.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/internal/ConverterAdapter.java
@@ -18,14 +18,18 @@
  */
 package org.apache.johnzon.mapper.internal;
 
-import org.apache.johnzon.mapper.Adapter;
 import org.apache.johnzon.mapper.Converter;
+import org.apache.johnzon.mapper.TypeAwareAdapter;
 
-public class ConverterAdapter<A> implements Adapter<A, String> {
+import java.lang.reflect.Type;
+
+public class ConverterAdapter<A> implements TypeAwareAdapter<A, String> {
     private final Converter<A> converter;
+    private final AdapterKey key;
 
-    public ConverterAdapter(final Converter<A> converter) {
+    public ConverterAdapter(final Converter<A> converter, final Type from) {
         this.converter = converter;
+        this.key = new AdapterKey(from, String.class);
     }
 
     public Converter<A> getConverter() {
@@ -41,4 +45,19 @@
     public String from(final A a) {
         return converter.toString(a);
     }
+
+    @Override
+    public Type getTo() {
+        return key.getTo();
+    }
+
+    @Override
+    public Type getFrom() {
+        return key.getFrom();
+    }
+
+    @Override
+    public AdapterKey getKey() {
+        return key;
+    }
 }
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/map/LazyConverterMap.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/map/LazyConverterMap.java
new file mode 100644
index 0000000..f1a4e42
--- /dev/null
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/map/LazyConverterMap.java
@@ -0,0 +1,637 @@
+/*
+ * 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.map;
+
+import org.apache.johnzon.mapper.Adapter;
+import org.apache.johnzon.mapper.Converter;
+import org.apache.johnzon.mapper.MapperException;
+import org.apache.johnzon.mapper.converter.BigDecimalConverter;
+import org.apache.johnzon.mapper.converter.BigIntegerConverter;
+import org.apache.johnzon.mapper.converter.ClassConverter;
+import org.apache.johnzon.mapper.converter.DateConverter;
+import org.apache.johnzon.mapper.converter.LocaleConverter;
+import org.apache.johnzon.mapper.converter.StringConverter;
+import org.apache.johnzon.mapper.converter.URIConverter;
+import org.apache.johnzon.mapper.converter.URLConverter;
+import org.apache.johnzon.mapper.internal.AdapterKey;
+import org.apache.johnzon.mapper.internal.ConverterAdapter;
+
+import java.lang.reflect.Type;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.net.URI;
+import java.net.URL;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.OffsetDateTime;
+import java.time.OffsetTime;
+import java.time.Period;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.TemporalAccessor;
+import java.time.temporal.TemporalQueries;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Set;
+import java.util.SimpleTimeZone;
+import java.util.TimeZone;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+import static java.time.temporal.ChronoField.DAY_OF_MONTH;
+import static java.time.temporal.ChronoField.HOUR_OF_DAY;
+import static java.time.temporal.ChronoField.MILLI_OF_SECOND;
+import static java.time.temporal.ChronoField.MINUTE_OF_HOUR;
+import static java.time.temporal.ChronoField.MONTH_OF_YEAR;
+import static java.time.temporal.ChronoField.SECOND_OF_MINUTE;
+import static java.time.temporal.ChronoField.YEAR;
+import static java.util.stream.Collectors.toSet;
+
+// important: override all usages,
+// mainly org.apache.johnzon.mapper.MapperConfig.findAdapter and
+// org.apache.johnzon.mapper.MappingParserImpl.findAdapter today
+public class LazyConverterMap extends ConcurrentHashMap<AdapterKey, Adapter<?, ?>> {
+    private static final Adapter<?, ?> NO_ADAPTER = new Adapter<Object, Object>() {
+        @Override
+        public Object to(final Object b) {
+            throw new UnsupportedOperationException("shouldn't be called");
+        }
+
+        @Override
+        public Object from(final Object a) {
+            return to(null); // just fail
+        }
+    };
+
+    private boolean useShortISO8601Format = true;
+    private DateTimeFormatter dateTimeFormatter;
+
+    public void setUseShortISO8601Format(final boolean useShortISO8601Format) {
+        this.useShortISO8601Format = useShortISO8601Format;
+    }
+
+    public void setDateTimeFormatter(final DateTimeFormatter dateTimeFormatter) {
+        this.dateTimeFormatter = dateTimeFormatter;
+    }
+
+    @Override
+    public Adapter<?, ?> get(final Object key) {
+        final Adapter<?, ?> found = super.get(key);
+        if (found == NO_ADAPTER) {
+            return null;
+        }
+        if (found == null) {
+            if (!AdapterKey.class.isInstance(key)) {
+                return null;
+            }
+            final AdapterKey k = AdapterKey.class.cast(key);
+            if (k.getTo() == String.class) {
+                final Adapter<?, ?> adapter = doLazyLookup(k);
+                if (adapter != null) {
+                    return adapter;
+                } // else let's cache we don't need to go through lazy lookups
+            }
+            add(k, NO_ADAPTER);
+            return null;
+        }
+        return found;
+    }
+
+    @Override
+    public Set<Entry<AdapterKey, Adapter<?, ?>>> entrySet() {
+        return super.entrySet().stream()
+                .filter(it -> it.getValue() != NO_ADAPTER)
+                .collect(toSet());
+    }
+
+    public Set<AdapterKey> adapterKeys() {
+        return Stream.concat(
+                super.keySet().stream()
+                        .filter(it -> super.get(it) != NO_ADAPTER),
+                Stream.of(Date.class, URI.class, URL.class, Class.class, String.class, BigDecimal.class, BigInteger.class,
+                        Locale.class, Period.class, Duration.class, Calendar.class, GregorianCalendar.class, TimeZone.class,
+                        ZoneId.class, ZoneOffset.class, SimpleTimeZone.class, Instant.class, LocalDateTime.class, LocalDate.class,
+                        ZonedDateTime.class, OffsetDateTime.class, OffsetTime.class)
+                        .map(it -> new AdapterKey(it, String.class, true)))
+                .collect(toSet());
+    }
+
+    private Adapter<?, ?> doLazyLookup(final AdapterKey key) {
+        final Type from = key.getFrom();
+        if (from == Date.class) {
+            return addDateConverter(key);
+        }
+        if (from == URI.class) {
+            return add(key, new ConverterAdapter<>(new URIConverter(), URI.class));
+        }
+        if (from == URL.class) {
+            return add(key, new ConverterAdapter<>(new URLConverter(), URL.class));
+        }
+        if (from == Class.class) {
+            return add(key, new ConverterAdapter<>(new ClassConverter(), Class.class));
+        }
+        if (from == String.class) {
+            return add(key, new ConverterAdapter<>(new StringConverter(), String.class));
+        }
+        if (from == BigDecimal.class) {
+            return add(key, new ConverterAdapter<>(new BigDecimalConverter(), BigDecimal.class));
+        }
+        if (from == BigInteger.class) {
+            return add(key, new ConverterAdapter<>(new BigIntegerConverter(), BigInteger.class));
+        }
+        if (from == Locale.class) {
+            return add(key, new LocaleConverter());
+        }
+        if (from == Period.class) {
+            return add(key, new ConverterAdapter<>(new Converter<Period>() {
+                @Override
+                public String toString(final Period instance) {
+                    return instance.toString();
+                }
+
+                @Override
+                public Period fromString(final String text) {
+                    return Period.parse(text);
+                }
+            }, Period.class));
+        }
+        if (from == Duration.class) {
+            return add(key, new ConverterAdapter<>(new Converter<Duration>() {
+                @Override
+                public String toString(final Duration instance) {
+                    return instance.toString();
+                }
+
+                @Override
+                public Duration fromString(final String text) {
+                    return Duration.parse(text);
+                }
+            }, Duration.class));
+        }
+        if (from == Calendar.class) {
+            return addCalendarConverter(key);
+        }
+        if (from == GregorianCalendar.class) {
+            return addGregorianCalendar(key);
+        }
+        if (from == TimeZone.class) {
+            return add(key, new ConverterAdapter<>(new Converter<TimeZone>() {
+                @Override
+                public String toString(final TimeZone instance) {
+                    return instance.getID();
+                }
+
+                @Override
+                public TimeZone fromString(final String text) {
+                    checkForDeprecatedTimeZone(text);
+                    return TimeZone.getTimeZone(text);
+                }
+            }, TimeZone.class));
+        }
+        if (from == ZoneId.class) {
+            return add(key, new ConverterAdapter<>(new Converter<ZoneId>() {
+                @Override
+                public String toString(final ZoneId instance) {
+                    return instance.getId();
+                }
+
+                @Override
+                public ZoneId fromString(final String text) {
+                    return ZoneId.of(text);
+                }
+            }, ZoneId.class));
+        }
+        if (from == ZoneOffset.class) {
+            return add(key, new ConverterAdapter<>(new Converter<ZoneOffset>() {
+                @Override
+                public String toString(final ZoneOffset instance) {
+                    return instance.getId();
+                }
+
+                @Override
+                public ZoneOffset fromString(final String text) {
+                    return ZoneOffset.of(text);
+                }
+            }, ZoneOffset.class));
+        }
+        if (from == SimpleTimeZone.class) {
+            return add(key, new ConverterAdapter<>(new Converter<SimpleTimeZone>() {
+                @Override
+                public String toString(final SimpleTimeZone instance) {
+                    return instance.getID();
+                }
+
+                @Override
+                public SimpleTimeZone fromString(final String text) {
+                    checkForDeprecatedTimeZone(text);
+                    final TimeZone timeZone = TimeZone.getTimeZone(text);
+                    return new SimpleTimeZone(timeZone.getRawOffset(), timeZone.getID());
+                }
+            }, SimpleTimeZone.class));
+        }
+        if (from == Instant.class) {
+            return addInstantConverter(key);
+        }
+        if (from == LocalDate.class) {
+            return addLocalDateConverter(key);
+        }
+        if (from == LocalTime.class) {
+            return add(key, new ConverterAdapter<>(new Converter<LocalTime>() {
+                @Override
+                public String toString(final LocalTime instance) {
+                    return instance.toString();
+                }
+
+                @Override
+                public LocalTime fromString(final String text) {
+                    return LocalTime.parse(text);
+                }
+            }, LocalTime.class));
+        }
+        if (from == LocalDateTime.class) {
+            return addLocalDateTimeConverter(key);
+        }
+        if (from == ZonedDateTime.class) {
+            return addZonedDateTimeConverter(key);
+        }
+        if (from == OffsetDateTime.class) {
+            return addOffsetDateTimeConverter(key);
+        }
+        if (from == OffsetTime.class) {
+            return add(key, new ConverterAdapter<>(new Converter<OffsetTime>() {
+                @Override
+                public String toString(final OffsetTime instance) {
+                    return instance.toString();
+                }
+
+                @Override
+                public OffsetTime fromString(final String text) {
+                    return OffsetTime.parse(text);
+                }
+            }, OffsetTime.class));
+        }
+        return null;
+    }
+
+    private Adapter<?, ?> addOffsetDateTimeConverter(final AdapterKey key) {
+        if (dateTimeFormatter != null) {
+            final ZoneId zoneIDUTC = ZoneId.of("UTC");
+            return add(key, new ConverterAdapter<>(new Converter<OffsetDateTime>() {
+                @Override
+                public String toString(final OffsetDateTime instance) {
+                    return dateTimeFormatter.format(ZonedDateTime.ofInstant(instance.toInstant(), zoneIDUTC));
+                }
+
+                @Override
+                public OffsetDateTime fromString(final String text) {
+                    try {
+                        return parseZonedDateTime(text, dateTimeFormatter, zoneIDUTC).toOffsetDateTime();
+                    } catch (final DateTimeParseException dpe) {
+                        return OffsetDateTime.parse(text);
+                    }
+                }
+            }, OffsetDateTime.class));
+        }
+        return add(key, new ConverterAdapter<>(new Converter<OffsetDateTime>() {
+            @Override
+            public String toString(final OffsetDateTime instance) {
+                return instance.toString();
+            }
+
+            @Override
+            public OffsetDateTime fromString(final String text) {
+                return OffsetDateTime.parse(text);
+            }
+        }, OffsetDateTime.class));
+    }
+
+    private Adapter<?, ?> addZonedDateTimeConverter(final AdapterKey key) {
+        if (dateTimeFormatter != null) {
+            final ZoneId zoneIDUTC = ZoneId.of("UTC");
+            return add(key, new ConverterAdapter<>(new Converter<ZonedDateTime>() {
+                @Override
+                public String toString(final ZonedDateTime instance) {
+                    return dateTimeFormatter.format(ZonedDateTime.ofInstant(instance.toInstant(), zoneIDUTC));
+                }
+
+                @Override
+                public ZonedDateTime fromString(final String text) {
+                    try {
+                        return parseZonedDateTime(text, dateTimeFormatter, zoneIDUTC);
+                    } catch (final DateTimeParseException dpe) {
+                        return ZonedDateTime.parse(text);
+                    }
+                }
+            }, ZonedDateTime.class));
+        }
+        return add(key, new ConverterAdapter<>(new Converter<ZonedDateTime>() {
+            @Override
+            public String toString(final ZonedDateTime instance) {
+                return instance.toString();
+            }
+
+            @Override
+            public ZonedDateTime fromString(final String text) {
+                return ZonedDateTime.parse(text);
+            }
+        }, ZonedDateTime.class));
+    }
+
+    private Adapter<?, ?> addLocalDateTimeConverter(final AdapterKey key) {
+        if (dateTimeFormatter != null) {
+            final ZoneId zoneIDUTC = ZoneId.of("UTC");
+            return add(key, new ConverterAdapter<>(new Converter<LocalDateTime>() {
+
+                @Override
+                public String toString(final LocalDateTime instance) {
+                    return dateTimeFormatter.format(ZonedDateTime.ofInstant(instance.toInstant(ZoneOffset.UTC), zoneIDUTC));
+                }
+
+                @Override
+                public LocalDateTime fromString(final String text) {
+                    try {
+                        return parseZonedDateTime(text, dateTimeFormatter, zoneIDUTC).toLocalDateTime();
+                    } catch (final DateTimeParseException dpe) {
+                        return LocalDateTime.parse(text);
+                    }
+                }
+            }, LocalDateTime.class));
+        }
+        return add(key, new ConverterAdapter<>(new Converter<LocalDateTime>() {
+            @Override
+            public String toString(final LocalDateTime instance) {
+                return instance.toString();
+            }
+
+            @Override
+            public LocalDateTime fromString(final String text) {
+                return LocalDateTime.parse(text);
+            }
+        }, LocalDateTime.class));
+    }
+
+    private Adapter<?, ?> addLocalDateConverter(final AdapterKey key) {
+        if (dateTimeFormatter != null) {
+            final ZoneId zoneIDUTC = ZoneId.of("UTC");
+            return add(key, new ConverterAdapter<>(new Converter<LocalDate>() {
+                @Override
+                public String toString(final LocalDate instance) {
+                    return dateTimeFormatter.format(ZonedDateTime.ofInstant(Instant.ofEpochMilli(TimeUnit.DAYS.toMillis(instance.toEpochDay())), zoneIDUTC));
+                }
+
+                @Override
+                public LocalDate fromString(final String text) {
+                    try {
+                        return parseZonedDateTime(text, dateTimeFormatter, zoneIDUTC).toLocalDate();
+                    } catch (final DateTimeParseException dpe) {
+                        return LocalDate.parse(text);
+                    }
+                }
+            }, LocalDate.class));
+        }
+        return add(key, new ConverterAdapter<>(new Converter<LocalDate>() {
+            @Override
+            public String toString(final LocalDate instance) {
+                return instance.toString();
+            }
+
+            @Override
+            public LocalDate fromString(final String text) {
+                return LocalDate.parse(text);
+            }
+        }, LocalDate.class));
+    }
+
+    private Adapter<?, ?> addInstantConverter(final AdapterKey key) {
+        if (dateTimeFormatter != null) {
+            final ZoneId zoneIDUTC = ZoneId.of("UTC");
+            return add(key, new ConverterAdapter<>(new Converter<Instant>() {
+                @Override
+                public String toString(final Instant instance) {
+                    return dateTimeFormatter.format(ZonedDateTime.ofInstant(instance, zoneIDUTC));
+                }
+
+                @Override
+                public Instant fromString(final String text) {
+                    return parseZonedDateTime(text, dateTimeFormatter, zoneIDUTC).toInstant();
+                }
+            }, Instant.class));
+        }
+        return add(key, new ConverterAdapter<>(new Converter<Instant>() {
+            @Override
+            public String toString(final Instant instance) {
+                return instance.toString();
+            }
+
+            @Override
+            public Instant fromString(final String text) {
+                return Instant.parse(text);
+            }
+        }, Instant.class));
+    }
+
+    private Adapter<?, ?> addGregorianCalendar(final AdapterKey key) {
+        if (dateTimeFormatter != null) {
+            final ZoneId zoneIDUTC = ZoneId.of("UTC");
+            return add(key, new ConverterAdapter<>(new Converter<GregorianCalendar>() {
+                @Override
+                public String toString(final GregorianCalendar instance) {
+                    return dateTimeFormatter.format(ZonedDateTime.ofInstant(instance.toInstant(), instance.getTimeZone().toZoneId()));
+                }
+
+                @Override
+                public GregorianCalendar fromString(final String text) {
+                    final ZonedDateTime zonedDateTime = parseZonedDateTime(text, dateTimeFormatter, zoneIDUTC);
+                    final Calendar instance = GregorianCalendar.getInstance();
+                    instance.setTimeZone(TimeZone.getTimeZone(zonedDateTime.getZone()));
+                    instance.setTime(Date.from(zonedDateTime.toInstant()));
+                    return GregorianCalendar.class.cast(instance);
+                }
+            }, GregorianCalendar.class));
+        }
+        return add(key, new ConverterAdapter<>(new Converter<GregorianCalendar>() {
+            @Override
+            public String toString(final GregorianCalendar instance) {
+                return toStringCalendar(instance);
+            }
+
+            @Override
+            public GregorianCalendar fromString(final String text) {
+                return fromCalendar(text, GregorianCalendar::from);
+            }
+        }, GregorianCalendar.class));
+    }
+
+    private Adapter<?, ?> addCalendarConverter(final AdapterKey key) {
+        if (dateTimeFormatter != null) {
+            final ZoneId zoneIDUTC = ZoneId.of("UTC");
+            return add(key, new ConverterAdapter<>(new Converter<Calendar>() {
+                @Override
+                public String toString(final Calendar instance) {
+                    return dateTimeFormatter.format(ZonedDateTime.ofInstant(instance.toInstant(), instance.getTimeZone().toZoneId()));
+                }
+
+                @Override
+                public Calendar fromString(final String text) {
+                    final ZonedDateTime zonedDateTime = parseZonedDateTime(text, dateTimeFormatter, zoneIDUTC);
+                    final Calendar instance = Calendar.getInstance();
+                    instance.setTimeZone(TimeZone.getTimeZone(zonedDateTime.getZone()));
+                    instance.setTime(Date.from(zonedDateTime.toInstant()));
+                    return instance;
+                }
+            }, Calendar.class));
+        }
+        return add(key, new ConverterAdapter<>(new Converter<Calendar>() {
+            @Override
+            public String toString(final Calendar instance) {
+                return toStringCalendar(instance);
+            }
+
+            @Override
+            public Calendar fromString(final String text) {
+                return fromCalendar(text, zdt -> {
+                    final Calendar instance = Calendar.getInstance();
+                    instance.clear();
+                    instance.setTimeZone(TimeZone.getTimeZone(zdt.getZone()));
+                    instance.setTimeInMillis(zdt.toInstant().toEpochMilli());
+                    return instance;
+                });
+            }
+        }, Calendar.class));
+    }
+
+    private Adapter<?, ?> addDateConverter(final AdapterKey key) {
+        if (useShortISO8601Format) {
+            return add(key, new ConverterAdapter<>(new DateConverter("yyyyMMddHHmmssZ"), Date.class));
+        }
+        final ZoneId zoneIDUTC = ZoneId.of("UTC");
+        if (dateTimeFormatter != null) {
+            return add(key, new ConverterAdapter<>(new Converter<Date>() {
+
+                @Override
+                public String toString(final Date instance) {
+                    return dateTimeFormatter.format(ZonedDateTime.ofInstant(instance.toInstant(), zoneIDUTC));
+                }
+
+                @Override
+                public Date fromString(final String text) {
+                    try {
+                        return Date.from(parseZonedDateTime(text, dateTimeFormatter, zoneIDUTC).toInstant());
+                    } catch (final DateTimeParseException dpe) {
+                        return Date.from(LocalDateTime.parse(text).toInstant(ZoneOffset.UTC));
+                    }
+                }
+            }, Date.class));
+        }
+        return add(key, new ConverterAdapter<>(new Converter<Date>() {
+            @Override
+            public String toString(final Date instance) {
+                return ZonedDateTime.ofInstant(instance.toInstant(), zoneIDUTC)
+                        .format(DateTimeFormatter.ISO_ZONED_DATE_TIME);
+            }
+
+            @Override
+            public Date fromString(final String text) {
+                try {
+                    return Date.from(ZonedDateTime.parse(text).toInstant());
+                } catch (final DateTimeParseException dte) {
+                    return Date.from(LocalDateTime.parse(text).toInstant(ZoneOffset.UTC));
+                }
+            }
+        }, Date.class));
+    }
+
+    private static ZonedDateTime parseZonedDateTime(final String text, final DateTimeFormatter formatter, final ZoneId defaultZone) {
+        final TemporalAccessor parse = formatter.parse(text);
+        ZoneId zone = parse.query(TemporalQueries.zone());
+        if (Objects.isNull(zone)) {
+            zone = defaultZone;
+        }
+        final int year = parse.isSupported(YEAR) ? parse.get(YEAR) : 0;
+        final int month = parse.isSupported(MONTH_OF_YEAR) ? parse.get(MONTH_OF_YEAR) : 0;
+        final int day = parse.isSupported(DAY_OF_MONTH) ? parse.get(DAY_OF_MONTH) : 0;
+        final int hour = parse.isSupported(HOUR_OF_DAY) ? parse.get(HOUR_OF_DAY) : 0;
+        final int minute = parse.isSupported(MINUTE_OF_HOUR) ? parse.get(MINUTE_OF_HOUR) : 0;
+        final int second = parse.isSupported(SECOND_OF_MINUTE) ? parse.get(SECOND_OF_MINUTE) : 0;
+        final int millisecond = parse.isSupported(MILLI_OF_SECOND) ? parse.get(MILLI_OF_SECOND) : 0;
+        return ZonedDateTime.of(year, month, day, hour, minute, second, millisecond, zone);
+    }
+
+    private static void checkForDeprecatedTimeZone(final String text) {
+        switch (text) {
+            case "CST": // really for TCK, this sucks for end users so we don't fail for all deprecated zones
+                throw new MapperException("Deprecated timezone: '" + text + '"');
+            default:
+        }
+    }
+
+    private String toStringCalendar(final Calendar instance) {
+        if (!hasTime(instance)) { // spec
+            final LocalDate localDate = LocalDate.of(
+                    instance.get(Calendar.YEAR),
+                    instance.get(Calendar.MONTH) + 1,
+                    instance.get(Calendar.DAY_OF_MONTH));
+            return localDate.toString() +
+                    (instance.getTimeZone() != null ?
+                            instance.getTimeZone().toZoneId().getRules()
+                                    .getOffset(Instant.ofEpochMilli(TimeUnit.DAYS.toMillis(localDate.toEpochDay()))) : "");
+        }
+        return ZonedDateTime.ofInstant(instance.toInstant(), instance.getTimeZone().toZoneId())
+                .format(DateTimeFormatter.ISO_DATE_TIME);
+    }
+
+    private boolean hasTime(final Calendar instance) {
+        if (!instance.isSet(Calendar.HOUR_OF_DAY)) {
+            return false;
+        }
+        return instance.get(Calendar.HOUR_OF_DAY) != 0 ||
+                (instance.isSet(Calendar.MINUTE) && instance.get(Calendar.MINUTE) != 0) ||
+                (instance.isSet(Calendar.SECOND) && instance.get(Calendar.SECOND) != 0);
+    }
+
+    private <T extends Calendar> T fromCalendar(final String text, final Function<ZonedDateTime, T> calendarSupplier) {
+        switch (text.length()) {
+            case 10: {
+                final ZonedDateTime date = LocalDate.parse(text)
+                        .atTime(0, 0, 0)
+                        .atZone(ZoneId.of("UTC"));
+                return calendarSupplier.apply(date);
+            }
+            default:
+                final ZonedDateTime zonedDateTime = ZonedDateTime.parse(text);
+                return calendarSupplier.apply(zonedDateTime);
+        }
+    }
+
+    private Adapter<?, ?> add(final AdapterKey key, final Adapter<?, ?> converter) {
+        final Adapter<?, ?> existing = putIfAbsent(key, converter);
+        return existing == null ? converter : existing;
+    }
+}
diff --git a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/MapperConfigTest.java b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/MapperConfigTest.java
index f0f4020..4044781 100644
--- a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/MapperConfigTest.java
+++ b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/MapperConfigTest.java
@@ -21,6 +21,7 @@
 import static java.util.Collections.emptyMap;
 
 import org.apache.johnzon.mapper.access.FieldAccessMode;
+import org.apache.johnzon.mapper.map.LazyConverterMap;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -31,7 +32,6 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
 
 public class MapperConfigTest {
 
@@ -156,7 +156,7 @@
 
 
     private MapperConfig createConfig(Map<Class<?>, ObjectConverter.Codec<?>> converter) {
-        return new MapperConfig(new ConcurrentHashMap<>(0),
+        return new MapperConfig(new LazyConverterMap(),
                                 Map.class.cast(converter), Map.class.cast(converter),
                                 -1,
                                 true,
diff --git a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/ObjectConverterTest.java b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/ObjectConverterTest.java
index 62e9495..8e5e059 100644
--- a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/ObjectConverterTest.java
+++ b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/ObjectConverterTest.java
@@ -108,6 +108,15 @@
             result = 31 * result + Arrays.hashCode(linkedPersonsArray);
             return result;
         }
+
+        @Override
+        public String toString() {
+            return "Contact{" +
+                    "linkedPersons=" + linkedPersons +
+                    ", linkedPersonsArray=" + Arrays.toString(linkedPersonsArray) +
+                    ", personMap=" + personMap +
+                    '}';
+        }
     }
 
     public static class Person {
@@ -156,5 +165,13 @@
             result = 31 * result + (lastName != null ? lastName.hashCode() : 0);
             return result;
         }
+
+        @Override
+        public String toString() {
+            return "Person{" +
+                    "firstName='" + firstName + '\'' +
+                    ", lastName='" + lastName + '\'' +
+                    '}';
+        }
     }
 }
\ No newline at end of file
diff --git a/johnzon-mapper/src/test/java/org/superbiz/ExtendMappingTest.java b/johnzon-mapper/src/test/java/org/superbiz/ExtendMappingTest.java
index 8e5264c..27fdd0d 100644
--- a/johnzon-mapper/src/test/java/org/superbiz/ExtendMappingTest.java
+++ b/johnzon-mapper/src/test/java/org/superbiz/ExtendMappingTest.java
@@ -21,13 +21,13 @@
 import org.apache.johnzon.mapper.MapperConfig;
 import org.apache.johnzon.mapper.Mappings;
 import org.apache.johnzon.mapper.access.FieldAccessMode;
+import org.apache.johnzon.mapper.map.LazyConverterMap;
 import org.junit.Test;
 
 import java.lang.reflect.Type;
 import java.nio.charset.StandardCharsets;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
 
 import static java.util.Collections.emptyMap;
 import static org.junit.Assert.assertEquals;
@@ -57,7 +57,7 @@
     public static class MyMappings extends Mappings {
         public MyMappings() {
             super(new MapperConfig(
-                    new ConcurrentHashMap<>(), new HashMap<>(), new HashMap<>(),
+                    new LazyConverterMap(), new HashMap<>(), new HashMap<>(),
                     -1, true, true, true, false, false, false,
                     new FieldAccessMode(false, false),
                     StandardCharsets.UTF_8, String::compareTo, false, false, null, false, false,