Merge pull request #131 from dblevins/annotation-symmetry

Annotation symmetry tests
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/Calls.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/Calls.java
new file mode 100644
index 0000000..83d1b88
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/Calls.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class Calls {
+
+    private final List<String> calls = new ArrayList<>();
+
+    public String called() {
+        final StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
+
+        if (stackTrace.length > 2) {
+            final StackTraceElement caller = stackTrace[2];
+            final String className = caller.getClassName();
+            final String methodName = caller.getMethodName();
+
+            final int lastDot = className.lastIndexOf('.');
+            final int lastDollar = className.lastIndexOf('$');
+            final String simpleName;
+            if (lastDollar == 0 && lastDot == 0) {
+                simpleName = className;
+            } else {
+                final int start = Math.max(lastDollar, lastDot) + 1;
+                simpleName = className.substring(start);
+            }
+
+            final String result = simpleName + "." + methodName;
+            calls.add(result);
+            return result;
+        }
+        return null;
+    }
+
+    public String called(final Object instance) {
+        final StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
+
+        if (stackTrace.length > 2) {
+            final StackTraceElement caller = stackTrace[2];
+            final String simpleName = instance.getClass().getSimpleName();
+            final String methodName = caller.getMethodName();
+
+
+            final String result = simpleName + "." + methodName;
+            calls.add(result);
+            return result;
+        }
+        return null;
+    }
+
+    public List<String> list() {
+        return new ArrayList<>(calls);
+    }
+
+    public String get() {
+        final String result = String.join("\n", calls);
+        calls.clear();
+        return result;
+    }
+
+    public void reset() {
+        calls.clear();
+    }
+}
\ No newline at end of file
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/SymmetryTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/SymmetryTest.java
new file mode 100644
index 0000000..2bf7a3b
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/SymmetryTest.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry;
+
+import jakarta.json.bind.Jsonb;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public abstract class SymmetryTest {
+
+    public abstract Jsonb jsonb();
+
+    public abstract void assertWrite(final Jsonb jsonb);
+
+    public abstract void assertRead(final Jsonb jsonb);
+
+    /**
+     * Assert a simple write operation
+     */
+    @Test
+    public void write() throws Exception {
+        try (final Jsonb jsonb = jsonb()) {
+            assertWrite(jsonb);
+        }
+    }
+
+    /**
+     * Assert a simple read operation
+     */
+    @Test
+    public void read() throws Exception {
+        try (final Jsonb jsonb = jsonb()) {
+            assertRead(jsonb);
+        }
+    }
+
+    /**
+     * Validate any caching done from a write operation
+     * leads to a consistent result on any future
+     * write operations
+     */
+    @Test
+    public void writeAfterWrite() throws Exception {
+        try (final Jsonb jsonb = jsonb()) {
+            assertWrite(jsonb);
+            assertWrite(jsonb);
+            assertWrite(jsonb);
+            assertWrite(jsonb);
+        }
+    }
+
+    /**
+     * Validate any caching done from a read operation
+     * leads to a consistent result on any future
+     * read operations
+     */
+    @Test
+    public void readAfterRead() throws Exception {
+        try (final Jsonb jsonb = jsonb()) {
+            assertRead(jsonb);
+            assertRead(jsonb);
+            assertRead(jsonb);
+            assertRead(jsonb);
+        }
+    }
+
+    /**
+     * Validate any caching done from a read operation
+     * does not alter the expected behavior of a write
+     * operation
+     */
+    @Test
+    public void writeAfterRead() throws Exception {
+        try (final Jsonb jsonb = jsonb()) {
+            assertRead(jsonb);
+            assertWrite(jsonb);
+            assertRead(jsonb);
+            assertWrite(jsonb);
+        }
+    }
+
+    /**
+     * Validate any caching done from a write operation
+     * does not alter the expected behavior of a read
+     * operation
+     */
+    @Test
+    public void readAfterWrite() throws Exception {
+        try (final Jsonb jsonb = jsonb()) {
+            assertWrite(jsonb);
+            assertRead(jsonb);
+            assertWrite(jsonb);
+            assertRead(jsonb);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterOnClassDirectTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterOnClassDirectTest.java
new file mode 100644
index 0000000..2249feb
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterOnClassDirectTest.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.array;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class ArrayAdapterOnClassDirectTest extends ArrayAdapterOnClassTest {
+
+    public Jsonb jsonb() {
+        return JsonbBuilder.create();
+    }
+
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final String json = jsonb.toJson(email);
+        assertEquals("[\"test\",\"domain.com\",\"EmailClass.adaptToJson\"]", json);
+        assertEquals("EmailClass.adaptToJson", calls());
+    }
+
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "[\"test\",\"domain.com\"]";
+        final Email email = jsonb.fromJson(json, Email.class);
+        assertEquals("test@domain.com:EmailClass.adaptFromJson", email.toString());
+        assertEquals("EmailClass.adaptFromJson", calls());
+    }
+
+    /**
+     * Fails as the adapter is not found
+     */
+    @Test
+    @Ignore()
+    @Override
+    public void read() throws Exception {
+        super.read();
+    }
+
+    /**
+     * Fails as the adapter is not found
+     */
+    @Test
+    @Ignore()
+    @Override
+    public void readAfterRead() throws Exception {
+        super.readAfterRead();
+    }
+
+    /**
+     * Fails as the adapter is not found on the first read
+     */
+    @Test
+    @Ignore()
+    @Override
+    public void writeAfterRead() throws Exception {
+        super.writeAfterRead();
+    }
+
+    @Test
+    @Ignore()
+    @Override
+    public void readAfterWrite() throws Exception {
+        super.readAfterWrite();
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterOnClassSimpleTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterOnClassSimpleTest.java
new file mode 100644
index 0000000..f1d3913
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterOnClassSimpleTest.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.array;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+
+import static org.junit.Assert.assertEquals;
+
+public class ArrayAdapterOnClassSimpleTest extends ArrayAdapterOnClassTest {
+
+    public Jsonb jsonb() {
+        return JsonbBuilder.create();
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":[\"test\",\"domain.com\"]}";
+        final ArrayAdapterPrecedenceConfigClassTest.Contact actual = jsonb.fromJson(json, ArrayAdapterPrecedenceConfigClassTest.Contact.class);
+        assertEquals("Contact{email=test@domain.com:EmailClass.adaptFromJson}", actual.toString());
+        assertEquals("Contact.<init>\n" +
+                "EmailClass.adaptFromJson\n" +
+                "Contact.setEmail", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final ArrayAdapterPrecedenceConfigClassTest.Contact contact = new ArrayAdapterPrecedenceConfigClassTest.Contact();
+        contact.setEmail(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":[\"test\",\"domain.com\",\"EmailClass.adaptToJson\"]}", json);
+        assertEquals("Contact.getEmail\n" +
+                "EmailClass.adaptToJson", calls());
+    }
+
+    public static class Contact {
+
+        private Email email;
+
+        public Contact() {
+            CALLS.called();
+        }
+
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+        public void setEmail(final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterOnClassTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterOnClassTest.java
new file mode 100644
index 0000000..dacc75e
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterOnClassTest.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.array;
+
+import jakarta.json.bind.adapter.JsonbAdapter;
+import jakarta.json.bind.annotation.JsonbTypeAdapter;
+import org.apache.johnzon.jsonb.symmetry.Calls;
+import org.apache.johnzon.jsonb.symmetry.SymmetryTest;
+import org.junit.Before;
+
+public abstract class ArrayAdapterOnClassTest extends SymmetryTest {
+
+    protected static final Calls CALLS = new Calls();
+
+    @Before
+    public void reset() {
+        CALLS.reset();
+    }
+
+    public static String calls() {
+        return CALLS.get();
+    }
+
+    @JsonbTypeAdapter(Adapter.EmailClass.class)
+    public static class Email {
+        final String user;
+        final String domain;
+        final String call;
+
+        public Email(final String user, final String domain) {
+            this(user, domain, null);
+        }
+
+        public Email(final String user, final String domain, final String call) {
+            this.user = user;
+            this.domain = domain;
+            this.call = call;
+        }
+
+        @Override
+        public String toString() {
+            if (call == null) {
+                return user + "@" + domain;
+            } else {
+                return user + "@" + domain + ":" + call;
+            }
+        }
+    }
+
+    public abstract static class Adapter implements JsonbAdapter<Email, String[]> {
+
+        @Override
+        public String[] adaptToJson(final Email obj) {
+            return new String[]{obj.user, obj.domain, CALLS.called(this)};
+        }
+
+        @Override
+        public Email adaptFromJson(final String[] parts) {
+            return new Email(parts[0], parts[1], CALLS.called(this));
+        }
+
+        public static final class Getter extends Adapter {
+        }
+
+        public static final class Setter extends Adapter {
+        }
+
+        public static final class Field extends Adapter {
+        }
+
+        public static final class Constructor extends Adapter {
+        }
+
+        public static final class Config extends Adapter {
+        }
+
+        public static final class EmailClass extends Adapter {
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterPrecedenceConfigClassConstructorHasGetterFinalFieldTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterPrecedenceConfigClassConstructorHasGetterFinalFieldTest.java
new file mode 100644
index 0000000..ed4e580
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterPrecedenceConfigClassConstructorHasGetterFinalFieldTest.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.array;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbTypeAdapter;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * JsonbTypeAdapter on
+ *  - Constructor
+ *  - Config
+ *  - Class
+ *
+ * Has
+ *  - Getter
+ *  - Final Field
+ *
+ * Outcome:
+ *  - Constructor wins on read
+ *  - EmailClass wins on write
+ *
+ * Question:
+ *  - Should Config win on write?
+ *    Adapters on the target type itself (Email) are effectively a hardcoded default adapter
+ *    If a user wishes to alter this behavior for a specific operation via the config, why
+ *    not let them?  This would be the most (only?) convenient way to change behavior without
+ *    sweeping code change.
+ */
+public class ArrayAdapterPrecedenceConfigClassConstructorHasGetterFinalFieldTest extends ArrayAdapterOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig().withAdapters(new Adapter.Config()));
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":[\"test\",\"domain.com\"]}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:Constructor.adaptFromJson}", actual.toString());
+        assertEquals("Constructor.adaptFromJson\n" +
+                "Contact.<init>", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":[\"test\",\"domain.com\",\"EmailClass.adaptToJson\"]}", json);
+        assertEquals("Contact.getEmail\n" +
+                "EmailClass.adaptToJson", calls());
+    }
+
+    public static class Contact {
+
+        private final Email email;
+
+        @JsonbCreator
+        public Contact(@JsonbProperty("email") @JsonbTypeAdapter(Adapter.Constructor.class) final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterPrecedenceConfigClassConstructorHasGetterSetterTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterPrecedenceConfigClassConstructorHasGetterSetterTest.java
new file mode 100644
index 0000000..56fda0d
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterPrecedenceConfigClassConstructorHasGetterSetterTest.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.array;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbTypeAdapter;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * JsonbTypeAdapter on
+ *  - Constructor
+ *  - Config
+ *  - Class
+ *
+ * Has
+ *  - Getter
+ *  - Setter
+ *
+ * Outcome:
+ *   - EmailClass wins on read
+ *   - EmailClass wins on write
+ *   - Constructor adapter is called, but overwritten
+ *
+ * Question:
+ *  - Should Config win on write?
+ *    Adapters on the target type itself (Email) are effectively a hardcoded default adapter
+ *    If a user wishes to alter this behavior for a specific operation via the config, why
+ *    not let them?  This would be the most (only?) convenient way to change behavior without
+ *    sweeping code change.
+ *
+ *  - Shouldn't the constructor win on read?
+ *    Seems like a clear bug
+ */
+public class ArrayAdapterPrecedenceConfigClassConstructorHasGetterSetterTest extends ArrayAdapterOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig().withAdapters(new Adapter.Config()));
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":[\"test\",\"domain.com\"]}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:EmailClass.adaptFromJson}", actual.toString());
+        assertEquals("Constructor.adaptFromJson\n" +
+                "Contact.<init>\n" +
+                "EmailClass.adaptFromJson\n" +
+                "Contact.setEmail", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":[\"test\",\"domain.com\",\"EmailClass.adaptToJson\"]}", json);
+        assertEquals("Contact.getEmail\n" +
+                "EmailClass.adaptToJson", calls());
+    }
+
+    public static class Contact {
+
+        private Email email;
+
+        @JsonbCreator
+        public Contact(@JsonbProperty("email") @JsonbTypeAdapter(Adapter.Constructor.class) final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+        public void setEmail(final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterPrecedenceConfigClassDirectTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterPrecedenceConfigClassDirectTest.java
new file mode 100644
index 0000000..e3bfd57
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterPrecedenceConfigClassDirectTest.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.array;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * JsonbTypeAdapter on
+ *  - Config
+ *  - Class
+ *
+ *
+ * Outcome:
+ *  - Config wins on read
+ *  - Config wins on write
+ */
+public class ArrayAdapterPrecedenceConfigClassDirectTest extends ArrayAdapterOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig().withAdapters(new Adapter.Config()));
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "[\"test\",\"domain.com\"]";
+        final Email actual = jsonb.fromJson(json, Email.class);
+        assertEquals("test@domain.com:Config.adaptFromJson", actual.toString());
+        assertEquals("Config.adaptFromJson", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+
+        final String json = jsonb.toJson(email);
+        assertEquals("[\"test\",\"domain.com\",\"Config.adaptToJson\"]", json);
+        assertEquals("Config.adaptToJson", calls());
+    }
+
+    @Test
+    @Ignore
+    @Override
+    public void read() throws Exception {
+        super.read();
+    }
+
+    @Test
+    @Ignore
+    @Override
+    public void readAfterRead() throws Exception {
+        super.readAfterRead();
+    }
+
+    @Test
+    @Ignore
+    @Override
+    public void readAfterWrite() throws Exception {
+        super.readAfterWrite();
+    }
+
+    @Test
+    @Ignore
+    @Override
+    public void writeAfterRead() throws Exception {
+        super.writeAfterRead();
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterPrecedenceConfigClassFieldConstructorHasGetterFinalFieldTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterPrecedenceConfigClassFieldConstructorHasGetterFinalFieldTest.java
new file mode 100644
index 0000000..a8fc42c
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterPrecedenceConfigClassFieldConstructorHasGetterFinalFieldTest.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.array;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbTypeAdapter;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * JsonbTypeAdapter on
+ *  - Field
+ *  - Constructor
+ *  - Config
+ *  - Class
+ *
+ * Has
+ *  - Getter
+ *  - Final Field
+ *
+ *  Outcome:
+ *   - Constructor wins on read
+ *   - Field wins on write
+ */
+public class ArrayAdapterPrecedenceConfigClassFieldConstructorHasGetterFinalFieldTest extends ArrayAdapterOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig().withAdapters(new Adapter.Config()));
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":[\"test\",\"domain.com\"]}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:Constructor.adaptFromJson}", actual.toString());
+        assertEquals("Constructor.adaptFromJson\n" +
+                "Contact.<init>", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":[\"test\",\"domain.com\",\"Field.adaptToJson\"]}", json);
+        assertEquals("Contact.getEmail\n" +
+                "Field.adaptToJson", calls());
+    }
+
+    public static class Contact {
+
+        @JsonbTypeAdapter(Adapter.Field.class)
+        private final Email email;
+
+        @JsonbCreator
+        public Contact(@JsonbProperty("email") @JsonbTypeAdapter(Adapter.Constructor.class) final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterPrecedenceConfigClassFieldConstructorHasGetterSetterTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterPrecedenceConfigClassFieldConstructorHasGetterSetterTest.java
new file mode 100644
index 0000000..9856bb5
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterPrecedenceConfigClassFieldConstructorHasGetterSetterTest.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.array;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbTypeAdapter;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * JsonbTypeAdapter on
+ *  - Field
+ *  - Constructor
+ *  - Config
+ *  - Class
+ *
+ * Has
+ *  - Getter
+ *  - Setter
+ *
+ *  Outcome:
+ *   - Field wins on read
+ *   - Field wins on write
+ *   - Constructor adapter is called, but overwritten
+ */
+public class ArrayAdapterPrecedenceConfigClassFieldConstructorHasGetterSetterTest extends ArrayAdapterOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig().withAdapters(new Adapter.Config()));
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":[\"test\",\"domain.com\"]}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:Field.adaptFromJson}", actual.toString());
+        assertEquals("Constructor.adaptFromJson\n" +
+                "Contact.<init>\n" +
+                "Field.adaptFromJson\n" +
+                "Contact.setEmail", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":[\"test\",\"domain.com\",\"Field.adaptToJson\"]}", json);
+        assertEquals("Contact.getEmail\n" +
+                "Field.adaptToJson", calls());
+    }
+
+    public static class Contact {
+
+        @JsonbTypeAdapter(Adapter.Field.class)
+        private Email email;
+
+        @JsonbCreator
+        public Contact(@JsonbProperty("email") @JsonbTypeAdapter(Adapter.Constructor.class) final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+        public void setEmail(final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterPrecedenceConfigClassGetterFieldConstructorHasSetterTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterPrecedenceConfigClassGetterFieldConstructorHasSetterTest.java
new file mode 100644
index 0000000..062b415
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterPrecedenceConfigClassGetterFieldConstructorHasSetterTest.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.array;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbTypeAdapter;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * JsonbTypeAdapter on
+ *  - Config
+ *  - Class
+ *  - Constructor
+ *  - Getter
+ *  - Field
+ *
+ * Still has a setter
+ *
+ * Outcome
+ *
+ *  - Field wins on read
+ *  - Getter wins on write
+ *  - Constructor adapter is called, but overwritten
+ */
+public class ArrayAdapterPrecedenceConfigClassGetterFieldConstructorHasSetterTest extends ArrayAdapterOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig().withAdapters(new Adapter.Config()));
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":[\"test\",\"domain.com\",\"Field.adaptToJson\"]}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:Field.adaptFromJson}", actual.toString());
+        assertEquals("Constructor.adaptFromJson\n" +
+                "Contact.<init>\n" +
+                "Field.adaptFromJson\n" +
+                "Contact.setEmail", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":[\"test\",\"domain.com\",\"Getter.adaptToJson\"]}", json);
+        assertEquals("Contact.getEmail\n" +
+                "Getter.adaptToJson", calls());
+    }
+
+    public static class Contact {
+
+        @JsonbTypeAdapter(Adapter.Field.class)
+        private Email email;
+
+        @JsonbCreator
+        public Contact(@JsonbProperty("email") @JsonbTypeAdapter(Adapter.Constructor.class) final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @JsonbTypeAdapter(Adapter.Getter.class)
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+        public void setEmail(final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterPrecedenceConfigClassGetterSetterFieldConstructorTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterPrecedenceConfigClassGetterSetterFieldConstructorTest.java
new file mode 100644
index 0000000..ace2697
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterPrecedenceConfigClassGetterSetterFieldConstructorTest.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.array;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbTypeAdapter;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * JsonbTypeAdapter on
+ *  - Field
+ *  - Constructor
+ *  - Getter
+ *  - Setter
+ *  - Config
+ *  - Class
+ *
+ *  Setter wins on read
+ *  Getter wins on write
+ *
+ *  Constructor adapter is called, but overwritten
+ */
+public class ArrayAdapterPrecedenceConfigClassGetterSetterFieldConstructorTest extends ArrayAdapterOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig().withAdapters(new Adapter.Config()));
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":[\"test\",\"domain.com\",\"Getter.adaptToJson\"]}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:Setter.adaptFromJson}", actual.toString());
+        assertEquals("Constructor.adaptFromJson\n" +
+                "Contact.<init>\n" +
+                "Setter.adaptFromJson\n" +
+                "Contact.setEmail", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":[\"test\",\"domain.com\",\"Getter.adaptToJson\"]}", json);
+        assertEquals("Contact.getEmail\n" +
+                "Getter.adaptToJson", calls());
+    }
+
+    public static class Contact {
+
+        @JsonbTypeAdapter(Adapter.Field.class)
+        private Email email;
+
+        @JsonbCreator
+        public Contact(@JsonbProperty("email") @JsonbTypeAdapter(Adapter.Constructor.class) final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @JsonbTypeAdapter(Adapter.Getter.class)
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+        @JsonbTypeAdapter(Adapter.Setter.class)
+        public void setEmail(final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterPrecedenceConfigClassSetterFieldConstructorHasGetterTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterPrecedenceConfigClassSetterFieldConstructorHasGetterTest.java
new file mode 100644
index 0000000..52eeae7
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterPrecedenceConfigClassSetterFieldConstructorHasGetterTest.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.array;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbTypeAdapter;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * JsonbTypeAdapter on
+ *  - Field
+ *  - Constructor
+ *  - Setter
+ *  - Config
+ *  - Class
+ *
+ * Still has a getter
+ *
+ * Outcome
+ *  - Setter wins on read
+ *  - Field wins on write
+ *  - Constructor adapter is called, but overwritten
+ */
+public class ArrayAdapterPrecedenceConfigClassSetterFieldConstructorHasGetterTest extends ArrayAdapterOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig().withAdapters(new Adapter.Config()));
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":[\"test\",\"domain.com\",\"Field.adaptToJson\"]}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:Setter.adaptFromJson}", actual.toString());
+        assertEquals("Constructor.adaptFromJson\n" +
+                "Contact.<init>\n" +
+                "Setter.adaptFromJson\n" +
+                "Contact.setEmail", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":[\"test\",\"domain.com\",\"Field.adaptToJson\"]}", json);
+        assertEquals("Contact.getEmail\n" +
+                "Field.adaptToJson", calls());
+    }
+
+    public static class Contact {
+
+        @JsonbTypeAdapter(Adapter.Field.class)
+        private Email email;
+
+        @JsonbCreator
+        public Contact(@JsonbProperty("email") @JsonbTypeAdapter(Adapter.Constructor.class) final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+        @JsonbTypeAdapter(Adapter.Setter.class)
+        public void setEmail(final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterPrecedenceConfigClassTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterPrecedenceConfigClassTest.java
new file mode 100644
index 0000000..f757f7a
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/array/ArrayAdapterPrecedenceConfigClassTest.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.array;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * JsonbTypeAdapter on
+ *  - Config
+ *  - Class
+ *
+ * Has
+ *  - Getter
+ *  - Final Field
+ *
+ * Outcome:
+ *  - EmailClass wins on read
+ *  - EmailClass wins on write
+ *
+ * Question:
+ *  - Should Config win on read and write?
+ *    Adapters on the target type itself (Email) are effectively a hardcoded default adapter
+ *    If a user wishes to alter this behavior for a specific operation via the config, why
+ *    not let them?  This would be the most (only?) convenient way to change behavior without
+ *    sweeping code change.
+ */
+public class ArrayAdapterPrecedenceConfigClassTest extends ArrayAdapterOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig().withAdapters(new Adapter.Config()));
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":[\"test\",\"domain.com\"]}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:EmailClass.adaptFromJson}", actual.toString());
+        assertEquals("Contact.<init>\n" +
+                "EmailClass.adaptFromJson\n" +
+                "Contact.setEmail", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact();
+        contact.setEmail(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":[\"test\",\"domain.com\",\"EmailClass.adaptToJson\"]}", json);
+        assertEquals("Contact.getEmail\n" +
+                "EmailClass.adaptToJson", calls());
+    }
+
+    public static class Contact {
+
+        private Email email;
+
+        public Contact() {
+            CALLS.called();
+        }
+
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+        public void setEmail(final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterOnClassDirectTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterOnClassDirectTest.java
new file mode 100644
index 0000000..a890546
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterOnClassDirectTest.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.map;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import org.junit.Ignore;
+
+import static org.junit.Assert.assertEquals;
+
+@Ignore("java.lang.ClassCastException: Cannot cast sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl to java.lang.Class")
+public class MapAdapterOnClassDirectTest extends MapAdapterOnClassTest {
+
+    public Jsonb jsonb() {
+        return JsonbBuilder.create();
+    }
+
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final String json = jsonb.toJson(email);
+        assertEquals("{\"user\":\"test\",\"domain\":\"domain.com\",\"call\":\"EmailClass.adaptToJson\"}", json);
+        assertEquals("EmailClass.adaptToJson", calls());
+    }
+
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"user\":\"test\",\"domain\":\"domain.com\"}";
+        final Email email = jsonb.fromJson(json, Email.class);
+        assertEquals("test@domain.com:EmailClass.adaptFromJson", email.toString());
+        assertEquals("EmailClass.adaptFromJson", calls());
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterOnClassSimpleTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterOnClassSimpleTest.java
new file mode 100644
index 0000000..68281d4
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterOnClassSimpleTest.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.map;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+
+import static org.junit.Assert.assertEquals;
+
+public class MapAdapterOnClassSimpleTest extends MapAdapterOnClassTest {
+
+    public Jsonb jsonb() {
+        return JsonbBuilder.create();
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\"}}";
+        final MapAdapterPrecedenceConfigClassTest.Contact actual = jsonb.fromJson(json, MapAdapterPrecedenceConfigClassTest.Contact.class);
+        assertEquals("Contact{email=test@domain.com:EmailClass.adaptFromJson}", actual.toString());
+        assertEquals("Contact.<init>\n" +
+                "EmailClass.adaptFromJson\n" +
+                "Contact.setEmail", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final MapAdapterPrecedenceConfigClassTest.Contact contact = new MapAdapterPrecedenceConfigClassTest.Contact();
+        contact.setEmail(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\",\"call\":\"EmailClass.adaptToJson\"}}", json);
+        assertEquals("Contact.getEmail\n" +
+                "EmailClass.adaptToJson", calls());
+    }
+
+    public static class Contact {
+
+        private Email email;
+
+        public Contact() {
+            CALLS.called();
+        }
+
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+        public void setEmail(final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterOnClassTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterOnClassTest.java
new file mode 100644
index 0000000..396f35d
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterOnClassTest.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.map;
+
+import jakarta.json.bind.adapter.JsonbAdapter;
+import jakarta.json.bind.annotation.JsonbTypeAdapter;
+import org.apache.johnzon.jsonb.symmetry.Calls;
+import org.apache.johnzon.jsonb.symmetry.SymmetryTest;
+import org.junit.Before;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public abstract class MapAdapterOnClassTest extends SymmetryTest {
+
+    protected static final Calls CALLS = new Calls();
+
+    @Before
+    public void reset() {
+        CALLS.reset();
+    }
+
+    public static String calls() {
+        return CALLS.get();
+    }
+
+    @JsonbTypeAdapter(Adapter.EmailClass.class)
+    public static class Email {
+        final String user;
+        final String domain;
+        final String call;
+
+        public Email(final String user, final String domain) {
+            this(user, domain, null);
+        }
+
+        public Email(final String user, final String domain, final String call) {
+            this.user = user;
+            this.domain = domain;
+            this.call = call;
+        }
+
+        @Override
+        public String toString() {
+            if (call == null) {
+                return user + "@" + domain;
+            } else {
+                return user + "@" + domain + ":" + call;
+            }
+        }
+    }
+
+    public abstract static class Adapter implements JsonbAdapter<Email, Map<String, Object>> {
+
+        @Override
+        public Map<String, Object> adaptToJson(final Email obj) {
+            final LinkedHashMap<String, Object> map = new LinkedHashMap<>();
+            map.put("user", obj.user);
+            map.put("domain", obj.domain);
+            map.put("call", CALLS.called(this));
+            return map;
+        }
+
+        @Override
+        public Email adaptFromJson(final Map<String, Object> map) {
+            return new Email(map.get("user").toString(),
+                    map.get("domain").toString(),
+                    CALLS.called(this));
+        }
+
+        public static final class Getter extends Adapter {
+        }
+
+        public static final class Setter extends Adapter {
+        }
+
+        public static final class Field extends Adapter {
+        }
+
+        public static final class Constructor extends Adapter {
+        }
+
+        public static final class Config extends Adapter {
+        }
+
+        public static final class EmailClass extends Adapter {
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterPrecedenceConfigClassConstructorHasGetterFinalFieldTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterPrecedenceConfigClassConstructorHasGetterFinalFieldTest.java
new file mode 100644
index 0000000..66a53bf
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterPrecedenceConfigClassConstructorHasGetterFinalFieldTest.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.map;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbTypeAdapter;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * JsonbTypeAdapter on
+ *  - Constructor
+ *  - Config
+ *  - Class
+ *
+ * Has
+ *  - Getter
+ *  - Final Field
+ *
+ * Outcome:
+ *  - Constructor wins on read
+ *  - EmailClass wins on write
+ *
+ * Question:
+ *  - Should Config win on write?
+ *    Adapters on the target type itself (Email) are effectively a hardcoded default adapter
+ *    If a user wishes to alter this behavior for a specific operation via the config, why
+ *    not let them?  This would be the most (only?) convenient way to change behavior without
+ *    sweeping code change.
+ */
+public class MapAdapterPrecedenceConfigClassConstructorHasGetterFinalFieldTest extends MapAdapterOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig().withAdapters(new Adapter.Config()));
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\"}}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:Constructor.adaptFromJson}", actual.toString());
+        assertEquals("Constructor.adaptFromJson\n" +
+                "Contact.<init>", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\",\"call\":\"EmailClass.adaptToJson\"}}", json);
+        assertEquals("Contact.getEmail\n" +
+                "EmailClass.adaptToJson", calls());
+    }
+
+    public static class Contact {
+
+        private final Email email;
+
+        @JsonbCreator
+        public Contact(@JsonbProperty("email") @JsonbTypeAdapter(Adapter.Constructor.class) final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterPrecedenceConfigClassConstructorHasGetterSetterTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterPrecedenceConfigClassConstructorHasGetterSetterTest.java
new file mode 100644
index 0000000..7eeb6d5
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterPrecedenceConfigClassConstructorHasGetterSetterTest.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.map;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbTypeAdapter;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * JsonbTypeAdapter on
+ *  - Constructor
+ *  - Config
+ *  - Class
+ *
+ * Has
+ *  - Getter
+ *  - Setter
+ *
+ * Outcome:
+ *   - EmailClass wins on read
+ *   - EmailClass wins on write
+ *   - Constructor adapter is called, but overwritten
+ *
+ * Question:
+ *  - Should Config win on write?
+ *    Adapters on the target type itself (Email) are effectively a hardcoded default adapter
+ *    If a user wishes to alter this behavior for a specific operation via the config, why
+ *    not let them?  This would be the most (only?) convenient way to change behavior without
+ *    sweeping code change.
+ *
+ *  - Shouldn't the constructor win on read?
+ *    Seems like a clear bug
+ */
+public class MapAdapterPrecedenceConfigClassConstructorHasGetterSetterTest extends MapAdapterOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig().withAdapters(new Adapter.Config()));
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\"}}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:EmailClass.adaptFromJson}", actual.toString());
+        assertEquals("Constructor.adaptFromJson\n" +
+                "Contact.<init>\n" +
+                "EmailClass.adaptFromJson\n" +
+                "Contact.setEmail", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\",\"call\":\"EmailClass.adaptToJson\"}}", json);
+        assertEquals("Contact.getEmail\n" +
+                "EmailClass.adaptToJson", calls());
+    }
+
+    public static class Contact {
+
+        private Email email;
+
+        @JsonbCreator
+        public Contact(@JsonbProperty("email") @JsonbTypeAdapter(Adapter.Constructor.class) final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+        public void setEmail(final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterPrecedenceConfigClassDirectTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterPrecedenceConfigClassDirectTest.java
new file mode 100644
index 0000000..3d59920
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterPrecedenceConfigClassDirectTest.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.map;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * JsonbTypeAdapter on
+ *  - Config
+ *  - Class
+ *
+ *
+ * Outcome:
+ *  - Reads fail
+ *  - Config wins on write
+ */
+public class MapAdapterPrecedenceConfigClassDirectTest extends MapAdapterOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig().withAdapters(new Adapter.Config()));
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"user\":\"test\",\"domain\":\"domain.com\"}";
+        final Email actual = jsonb.fromJson(json, Email.class);
+        assertEquals("test@domain.com:Config.adaptFromJson", actual.toString());
+        assertEquals("Config.adaptFromJson", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+
+        final String json = jsonb.toJson(email);
+        assertEquals("{\"user\":\"test\",\"domain\":\"domain.com\",\"call\":\"Config.adaptToJson\"}", json);
+        assertEquals("Config.adaptToJson", calls());
+    }
+
+    /**
+     * java.lang.ClassCastException: Cannot cast sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl to java.lang.Class
+     *
+     *  at java.base/java.lang.Class.cast(Class.java:3889)
+     *  at org.apache.johnzon.jsonb.JsonbAccessMode.isReversedAdapter(JsonbAccessMode.java:875)
+     */
+    @Test
+    @Ignore
+    @Override
+    public void read() throws Exception {
+        super.read();
+    }
+
+    /**
+     * java.lang.ClassCastException: Cannot cast sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl to java.lang.Class
+     *
+     *  at java.base/java.lang.Class.cast(Class.java:3889)
+     *  at org.apache.johnzon.jsonb.JsonbAccessMode.isReversedAdapter(JsonbAccessMode.java:875)
+     */
+    @Test
+    @Ignore
+    @Override
+    public void readAfterRead() throws Exception {
+        super.readAfterRead();
+    }
+
+    /**
+     * java.lang.ClassCastException: Cannot cast sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl to java.lang.Class
+     *
+     *  at java.base/java.lang.Class.cast(Class.java:3889)
+     *  at org.apache.johnzon.jsonb.JsonbAccessMode.isReversedAdapter(JsonbAccessMode.java:875)
+     */
+    @Test
+    @Ignore
+    @Override
+    public void readAfterWrite() throws Exception {
+        super.readAfterWrite();
+    }
+
+    /**
+     * java.lang.ClassCastException: Cannot cast sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl to java.lang.Class
+     *
+     *  at java.base/java.lang.Class.cast(Class.java:3889)
+     *  at org.apache.johnzon.jsonb.JsonbAccessMode.isReversedAdapter(JsonbAccessMode.java:875)
+     */
+    @Test
+    @Ignore
+    @Override
+    public void writeAfterRead() throws Exception {
+        super.writeAfterRead();
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterPrecedenceConfigClassFieldConstructorHasGetterFinalFieldTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterPrecedenceConfigClassFieldConstructorHasGetterFinalFieldTest.java
new file mode 100644
index 0000000..581ffa4
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterPrecedenceConfigClassFieldConstructorHasGetterFinalFieldTest.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.map;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbTypeAdapter;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * JsonbTypeAdapter on
+ *  - Field
+ *  - Constructor
+ *  - Config
+ *  - Class
+ *
+ * Has
+ *  - Getter
+ *  - Final Field
+ *
+ *  Outcome:
+ *   - Constructor wins on read
+ *   - Field wins on write
+ */
+public class MapAdapterPrecedenceConfigClassFieldConstructorHasGetterFinalFieldTest extends MapAdapterOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig().withAdapters(new Adapter.Config()));
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\"}}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:Constructor.adaptFromJson}", actual.toString());
+        assertEquals("Constructor.adaptFromJson\n" +
+                "Contact.<init>", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\",\"call\":\"Field.adaptToJson\"}}", json);
+        assertEquals("Contact.getEmail\n" +
+                "Field.adaptToJson", calls());
+    }
+
+    public static class Contact {
+
+        @JsonbTypeAdapter(Adapter.Field.class)
+        private final Email email;
+
+        @JsonbCreator
+        public Contact(@JsonbProperty("email") @JsonbTypeAdapter(Adapter.Constructor.class) final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterPrecedenceConfigClassFieldConstructorHasGetterSetterTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterPrecedenceConfigClassFieldConstructorHasGetterSetterTest.java
new file mode 100644
index 0000000..67e4cab
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterPrecedenceConfigClassFieldConstructorHasGetterSetterTest.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.map;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbTypeAdapter;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * JsonbTypeAdapter on
+ *  - Field
+ *  - Constructor
+ *  - Config
+ *  - Class
+ *
+ * Has
+ *  - Getter
+ *  - Setter
+ *
+ *  Outcome:
+ *   - Field wins on read
+ *   - Field wins on write
+ *   - Constructor adapter is called, but overwritten
+ */
+public class MapAdapterPrecedenceConfigClassFieldConstructorHasGetterSetterTest extends MapAdapterOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig().withAdapters(new Adapter.Config()));
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\"}}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:Field.adaptFromJson}", actual.toString());
+        assertEquals("Constructor.adaptFromJson\n" +
+                "Contact.<init>\n" +
+                "Field.adaptFromJson\n" +
+                "Contact.setEmail", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\",\"call\":\"Field.adaptToJson\"}}", json);
+        assertEquals("Contact.getEmail\n" +
+                "Field.adaptToJson", calls());
+    }
+
+    public static class Contact {
+
+        @JsonbTypeAdapter(Adapter.Field.class)
+        private Email email;
+
+        @JsonbCreator
+        public Contact(@JsonbProperty("email") @JsonbTypeAdapter(Adapter.Constructor.class) final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+        public void setEmail(final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterPrecedenceConfigClassGetterFieldConstructorHasSetterTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterPrecedenceConfigClassGetterFieldConstructorHasSetterTest.java
new file mode 100644
index 0000000..6ba33d8
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterPrecedenceConfigClassGetterFieldConstructorHasSetterTest.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.map;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbTypeAdapter;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * JsonbTypeAdapter on
+ *  - Config
+ *  - Class
+ *  - Constructor
+ *  - Getter
+ *  - Field
+ *
+ * Still has a setter
+ *
+ * Outcome
+ *
+ *  - Field wins on read
+ *  - Getter wins on write
+ *  - Constructor adapter is called, but overwritten
+ */
+public class MapAdapterPrecedenceConfigClassGetterFieldConstructorHasSetterTest extends MapAdapterOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig().withAdapters(new Adapter.Config()));
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\"}}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:Field.adaptFromJson}", actual.toString());
+        assertEquals("Constructor.adaptFromJson\n" +
+                "Contact.<init>\n" +
+                "Field.adaptFromJson\n" +
+                "Contact.setEmail", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\",\"call\":\"Getter.adaptToJson\"}}", json);
+        assertEquals("Contact.getEmail\n" +
+                "Getter.adaptToJson", calls());
+    }
+
+    public static class Contact {
+
+        @JsonbTypeAdapter(Adapter.Field.class)
+        private Email email;
+
+        @JsonbCreator
+        public Contact(@JsonbProperty("email") @JsonbTypeAdapter(Adapter.Constructor.class) final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @JsonbTypeAdapter(Adapter.Getter.class)
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+        public void setEmail(final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterPrecedenceConfigClassGetterSetterFieldConstructorTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterPrecedenceConfigClassGetterSetterFieldConstructorTest.java
new file mode 100644
index 0000000..ece3468
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterPrecedenceConfigClassGetterSetterFieldConstructorTest.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.map;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbTypeAdapter;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * JsonbTypeAdapter on
+ *  - Field
+ *  - Constructor
+ *  - Getter
+ *  - Setter
+ *  - Config
+ *  - Class
+ *
+ *  Setter wins on read
+ *  Getter wins on write
+ *
+ *  Constructor adapter is called, but overwritten
+ */
+public class MapAdapterPrecedenceConfigClassGetterSetterFieldConstructorTest extends MapAdapterOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig().withAdapters(new Adapter.Config()));
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\"}}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:Setter.adaptFromJson}", actual.toString());
+        assertEquals("Constructor.adaptFromJson\n" +
+                "Contact.<init>\n" +
+                "Setter.adaptFromJson\n" +
+                "Contact.setEmail", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\",\"call\":\"Getter.adaptToJson\"}}", json);
+        assertEquals("Contact.getEmail\n" +
+                "Getter.adaptToJson", calls());
+    }
+
+    public static class Contact {
+
+        @JsonbTypeAdapter(Adapter.Field.class)
+        private Email email;
+
+        @JsonbCreator
+        public Contact(@JsonbProperty("email") @JsonbTypeAdapter(Adapter.Constructor.class) final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @JsonbTypeAdapter(Adapter.Getter.class)
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+        @JsonbTypeAdapter(Adapter.Setter.class)
+        public void setEmail(final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterPrecedenceConfigClassSetterFieldConstructorHasGetterTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterPrecedenceConfigClassSetterFieldConstructorHasGetterTest.java
new file mode 100644
index 0000000..e02f03c
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterPrecedenceConfigClassSetterFieldConstructorHasGetterTest.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.map;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbTypeAdapter;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * JsonbTypeAdapter on
+ *  - Field
+ *  - Constructor
+ *  - Setter
+ *  - Config
+ *  - Class
+ *
+ * Still has a getter
+ *
+ * Outcome
+ *  - Setter wins on read
+ *  - Field wins on write
+ *  - Constructor adapter is called, but overwritten
+ */
+public class MapAdapterPrecedenceConfigClassSetterFieldConstructorHasGetterTest extends MapAdapterOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig().withAdapters(new Adapter.Config()));
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\"}}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:Setter.adaptFromJson}", actual.toString());
+        assertEquals("Constructor.adaptFromJson\n" +
+                "Contact.<init>\n" +
+                "Setter.adaptFromJson\n" +
+                "Contact.setEmail", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\",\"call\":\"Field.adaptToJson\"}}", json);
+        assertEquals("Contact.getEmail\n" +
+                "Field.adaptToJson", calls());
+    }
+
+    public static class Contact {
+
+        @JsonbTypeAdapter(Adapter.Field.class)
+        private Email email;
+
+        @JsonbCreator
+        public Contact(@JsonbProperty("email") @JsonbTypeAdapter(Adapter.Constructor.class) final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+        @JsonbTypeAdapter(Adapter.Setter.class)
+        public void setEmail(final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterPrecedenceConfigClassTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterPrecedenceConfigClassTest.java
new file mode 100644
index 0000000..b4e5ee1
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/map/MapAdapterPrecedenceConfigClassTest.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.map;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * JsonbTypeAdapter on
+ *  - Config
+ *  - Class
+ *
+ * Has
+ *  - Getter
+ *  - Final Field
+ *
+ * Outcome:
+ *  - EmailClass wins on read
+ *  - EmailClass wins on write
+ *
+ * Question:
+ *  - Should Config win on read and write?
+ *    Adapters on the target type itself (Email) are effectively a hardcoded default adapter
+ *    If a user wishes to alter this behavior for a specific operation via the config, why
+ *    not let them?  This would be the most (only?) convenient way to change behavior without
+ *    sweeping code change.
+ */
+public class MapAdapterPrecedenceConfigClassTest extends MapAdapterOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig().withAdapters(new Adapter.Config()));
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\"}}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:EmailClass.adaptFromJson}", actual.toString());
+        assertEquals("Contact.<init>\n" +
+                "EmailClass.adaptFromJson\n" +
+                "Contact.setEmail", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact();
+        contact.setEmail(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\",\"call\":\"EmailClass.adaptToJson\"}}", json);
+        assertEquals("Contact.getEmail\n" +
+                "EmailClass.adaptToJson", calls());
+    }
+
+    public static class Contact {
+
+        private Email email;
+
+        public Contact() {
+            CALLS.called();
+        }
+
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+        public void setEmail(final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterOnClassDirectTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterOnClassDirectTest.java
new file mode 100644
index 0000000..7266a76
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterOnClassDirectTest.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.string;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class StringAdapterOnClassDirectTest extends StringAdapterOnClassTest {
+
+    public Jsonb jsonb() {
+        return JsonbBuilder.create();
+    }
+
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final String json = jsonb.toJson(email);
+        assertEquals("\"test@domain.com:EmailClass.adaptToJson\"", json);
+        assertEquals("EmailClass.adaptToJson", calls());
+    }
+
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "\"test@domain.com\"";
+        final Email email = jsonb.fromJson(json, Email.class);
+        assertEquals("test@domain.com:EmailClass.adaptFromJson", email.toString());
+        assertEquals("EmailClass.adaptFromJson", calls());
+    }
+
+    /**
+     * Fails as the adapter is not found
+     */
+    @Test
+    @Ignore()
+    @Override
+    public void read() throws Exception {
+        super.read();
+    }
+
+    /**
+     * Fails as the adapter is not found
+     */
+    @Test
+    @Ignore()
+    @Override
+    public void readAfterRead() throws Exception {
+        super.readAfterRead();
+    }
+
+    /**
+     * Fails as the adapter is not found on the first read
+     */
+    @Test
+    @Ignore()
+    @Override
+    public void writeAfterRead() throws Exception {
+        super.writeAfterRead();
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterOnClassSimpleTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterOnClassSimpleTest.java
new file mode 100644
index 0000000..011cab1
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterOnClassSimpleTest.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.string;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+
+import static org.junit.Assert.assertEquals;
+
+public class StringAdapterOnClassSimpleTest extends StringAdapterOnClassTest {
+
+    public Jsonb jsonb() {
+        return JsonbBuilder.create();
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":\"test@domain.com\"}";
+        final StringAdapterPrecedenceConfigClassTest.Contact actual = jsonb.fromJson(json, StringAdapterPrecedenceConfigClassTest.Contact.class);
+        assertEquals("Contact{email=test@domain.com:EmailClass.adaptFromJson}", actual.toString());
+        assertEquals("Contact.<init>\n" +
+                "EmailClass.adaptFromJson\n" +
+                "Contact.setEmail", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final StringAdapterPrecedenceConfigClassTest.Contact contact = new StringAdapterPrecedenceConfigClassTest.Contact();
+        contact.setEmail(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":\"test@domain.com:EmailClass.adaptToJson\"}", json);
+        assertEquals("Contact.getEmail\n" +
+                "EmailClass.adaptToJson", calls());
+    }
+
+    public static class Contact {
+
+        private Email email;
+
+        public Contact() {
+            CALLS.called();
+        }
+
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+        public void setEmail(final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterOnClassTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterOnClassTest.java
new file mode 100644
index 0000000..3f3ba0e
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterOnClassTest.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.string;
+
+import jakarta.json.bind.adapter.JsonbAdapter;
+import jakarta.json.bind.annotation.JsonbTypeAdapter;
+import org.apache.johnzon.jsonb.symmetry.Calls;
+import org.apache.johnzon.jsonb.symmetry.SymmetryTest;
+import org.junit.Before;
+
+public abstract class StringAdapterOnClassTest extends SymmetryTest {
+
+    protected static final Calls CALLS = new Calls();
+
+    @Before
+    public void reset() {
+        CALLS.reset();
+    }
+
+    public static String calls() {
+        return CALLS.get();
+    }
+
+    @JsonbTypeAdapter(Adapter.EmailClass.class)
+    public static class Email {
+        final String user;
+        final String domain;
+        final String call;
+
+        public Email(final String user, final String domain) {
+            this(user, domain, null);
+        }
+
+        public Email(final String user, final String domain, final String call) {
+            this.user = user;
+            this.domain = domain;
+            this.call = call;
+        }
+
+        @Override
+        public String toString() {
+            if (call == null) {
+                return user + "@" + domain;
+            } else {
+                return user + "@" + domain + ":" + call;
+            }
+        }
+    }
+
+    public abstract static class Adapter implements JsonbAdapter<Email, String> {
+
+        @Override
+        public String adaptToJson(final Email obj) {
+            return obj.user + "@" + obj.domain + ":" + CALLS.called(this);
+        }
+
+        @Override
+        public Email adaptFromJson(final String obj) {
+            final String[] parts = obj.split("[@:]");
+            return new Email(parts[0], parts[1], CALLS.called(this));
+        }
+
+        public static final class Getter extends Adapter {
+        }
+
+        public static final class Setter extends Adapter {
+        }
+
+        public static final class Field extends Adapter {
+        }
+
+        public static final class Constructor extends Adapter {
+        }
+
+        public static final class Config extends Adapter {
+        }
+
+        public static final class EmailClass extends Adapter {
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterPrecedenceConfigClassConstructorHasGetterFinalFieldTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterPrecedenceConfigClassConstructorHasGetterFinalFieldTest.java
new file mode 100644
index 0000000..0880bef
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterPrecedenceConfigClassConstructorHasGetterFinalFieldTest.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.string;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbTypeAdapter;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * JsonbTypeAdapter on
+ *  - Constructor
+ *  - Config
+ *  - Class
+ *
+ * Has
+ *  - Getter
+ *  - Final Field
+ *
+ * Outcome:
+ *  - Constructor wins on read
+ *  - EmailClass wins on write
+ *
+ * Question:
+ *  - Should Config win on write?
+ *    Adapters on the target type itself (Email) are effectively a hardcoded default adapter
+ *    If a user wishes to alter this behavior for a specific operation via the config, why
+ *    not let them?  This would be the most (only?) convenient way to change behavior without
+ *    sweeping code change.
+ */
+public class StringAdapterPrecedenceConfigClassConstructorHasGetterFinalFieldTest extends StringAdapterOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig().withAdapters(new Adapter.Config()));
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":\"test@domain.com\"}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:Constructor.adaptFromJson}", actual.toString());
+        assertEquals("Constructor.adaptFromJson\n" +
+                "Contact.<init>", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":\"test@domain.com:EmailClass.adaptToJson\"}", json);
+        assertEquals("Contact.getEmail\n" +
+                "EmailClass.adaptToJson", calls());
+    }
+
+    public static class Contact {
+
+        private final Email email;
+
+        @JsonbCreator
+        public Contact(@JsonbProperty("email") @JsonbTypeAdapter(Adapter.Constructor.class) final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterPrecedenceConfigClassConstructorHasGetterSetterTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterPrecedenceConfigClassConstructorHasGetterSetterTest.java
new file mode 100644
index 0000000..4321370
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterPrecedenceConfigClassConstructorHasGetterSetterTest.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.string;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbTypeAdapter;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * JsonbTypeAdapter on
+ *  - Constructor
+ *  - Config
+ *  - Class
+ *
+ * Has
+ *  - Getter
+ *  - Setter
+ *
+ * Outcome:
+ *   - EmailClass wins on read
+ *   - EmailClass wins on write
+ *   - Constructor adapter is called, but overwritten
+ *
+ * Question:
+ *  - Should Config win on write?
+ *    Adapters on the target type itself (Email) are effectively a hardcoded default adapter
+ *    If a user wishes to alter this behavior for a specific operation via the config, why
+ *    not let them?  This would be the most (only?) convenient way to change behavior without
+ *    sweeping code change.
+ *
+ *  - Shouldn't the constructor win on read?
+ *    Seems like a clear bug
+ */
+public class StringAdapterPrecedenceConfigClassConstructorHasGetterSetterTest extends StringAdapterOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig().withAdapters(new Adapter.Config()));
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":\"test@domain.com\"}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:EmailClass.adaptFromJson}", actual.toString());
+        assertEquals("Constructor.adaptFromJson\n" +
+                "Contact.<init>\n" +
+                "EmailClass.adaptFromJson\n" +
+                "Contact.setEmail", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":\"test@domain.com:EmailClass.adaptToJson\"}", json);
+        assertEquals("Contact.getEmail\n" +
+                "EmailClass.adaptToJson", calls());
+    }
+
+    public static class Contact {
+
+        private Email email;
+
+        @JsonbCreator
+        public Contact(@JsonbProperty("email") @JsonbTypeAdapter(Adapter.Constructor.class) final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+        public void setEmail(final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterPrecedenceConfigClassDirectTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterPrecedenceConfigClassDirectTest.java
new file mode 100644
index 0000000..4097087
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterPrecedenceConfigClassDirectTest.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.string;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * JsonbTypeAdapter on
+ *  - Config
+ *  - Class
+ *
+ *
+ * Outcome:
+ *  - Config wins on read
+ *  - Config wins on write
+ */
+public class StringAdapterPrecedenceConfigClassDirectTest extends StringAdapterOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig().withAdapters(new Adapter.Config()));
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "\"test@domain.com\"";
+        final Email actual = jsonb.fromJson(json, Email.class);
+        assertEquals("test@domain.com:Config.adaptFromJson", actual.toString());
+        assertEquals("Config.adaptFromJson", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+
+        final String json = jsonb.toJson(email);
+        assertEquals("\"test@domain.com:Config.adaptToJson\"", json);
+        assertEquals("Config.adaptToJson", calls());
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterPrecedenceConfigClassFieldConstructorHasGetterFinalFieldTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterPrecedenceConfigClassFieldConstructorHasGetterFinalFieldTest.java
new file mode 100644
index 0000000..38e0314
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterPrecedenceConfigClassFieldConstructorHasGetterFinalFieldTest.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.string;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbTypeAdapter;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * JsonbTypeAdapter on
+ *  - Field
+ *  - Constructor
+ *  - Config
+ *  - Class
+ *
+ * Has
+ *  - Getter
+ *  - Final Field
+ *
+ *  Outcome:
+ *   - Constructor wins on read
+ *   - Field wins on write
+ */
+public class StringAdapterPrecedenceConfigClassFieldConstructorHasGetterFinalFieldTest extends StringAdapterOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig().withAdapters(new Adapter.Config()));
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":\"test@domain.com\"}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:Constructor.adaptFromJson}", actual.toString());
+        assertEquals("Constructor.adaptFromJson\n" +
+                "Contact.<init>", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":\"test@domain.com:Field.adaptToJson\"}", json);
+        assertEquals("Contact.getEmail\n" +
+                "Field.adaptToJson", calls());
+    }
+
+    public static class Contact {
+
+        @JsonbTypeAdapter(Adapter.Field.class)
+        private final Email email;
+
+        @JsonbCreator
+        public Contact(@JsonbProperty("email") @JsonbTypeAdapter(Adapter.Constructor.class) final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterPrecedenceConfigClassFieldConstructorHasGetterSetterTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterPrecedenceConfigClassFieldConstructorHasGetterSetterTest.java
new file mode 100644
index 0000000..cf91f68
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterPrecedenceConfigClassFieldConstructorHasGetterSetterTest.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.string;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbTypeAdapter;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * JsonbTypeAdapter on
+ *  - Field
+ *  - Constructor
+ *  - Config
+ *  - Class
+ *
+ * Has
+ *  - Getter
+ *  - Setter
+ *
+ *  Outcome:
+ *   - Field wins on read
+ *   - Field wins on write
+ *   - Constructor adapter is called, but overwritten
+ */
+public class StringAdapterPrecedenceConfigClassFieldConstructorHasGetterSetterTest extends StringAdapterOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig().withAdapters(new Adapter.Config()));
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":\"test@domain.com\"}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:Field.adaptFromJson}", actual.toString());
+        assertEquals("Constructor.adaptFromJson\n" +
+                "Contact.<init>\n" +
+                "Field.adaptFromJson\n" +
+                "Contact.setEmail", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":\"test@domain.com:Field.adaptToJson\"}", json);
+        assertEquals("Contact.getEmail\n" +
+                "Field.adaptToJson", calls());
+    }
+
+    public static class Contact {
+
+        @JsonbTypeAdapter(Adapter.Field.class)
+        private Email email;
+
+        @JsonbCreator
+        public Contact(@JsonbProperty("email") @JsonbTypeAdapter(Adapter.Constructor.class) final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+        public void setEmail(final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterPrecedenceConfigClassGetterFieldConstructorHasSetterTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterPrecedenceConfigClassGetterFieldConstructorHasSetterTest.java
new file mode 100644
index 0000000..ac50bf1
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterPrecedenceConfigClassGetterFieldConstructorHasSetterTest.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.string;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbTypeAdapter;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * JsonbTypeAdapter on
+ *  - Config
+ *  - Class
+ *  - Constructor
+ *  - Getter
+ *  - Field
+ *
+ * Still has a setter
+ *
+ * Outcome
+ *
+ *  - Field wins on read
+ *  - Getter wins on write
+ *  - Constructor adapter is called, but overwritten
+ */
+public class StringAdapterPrecedenceConfigClassGetterFieldConstructorHasSetterTest extends StringAdapterOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig().withAdapters(new Adapter.Config()));
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":\"test@domain.com\"}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:Field.adaptFromJson}", actual.toString());
+        assertEquals("Constructor.adaptFromJson\n" +
+                "Contact.<init>\n" +
+                "Field.adaptFromJson\n" +
+                "Contact.setEmail", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":\"test@domain.com:Getter.adaptToJson\"}", json);
+        assertEquals("Contact.getEmail\n" +
+                "Getter.adaptToJson", calls());
+    }
+
+    public static class Contact {
+
+        @JsonbTypeAdapter(Adapter.Field.class)
+        private Email email;
+
+        @JsonbCreator
+        public Contact(@JsonbProperty("email") @JsonbTypeAdapter(Adapter.Constructor.class) final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @JsonbTypeAdapter(Adapter.Getter.class)
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+        public void setEmail(final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterPrecedenceConfigClassGetterSetterFieldConstructorTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterPrecedenceConfigClassGetterSetterFieldConstructorTest.java
new file mode 100644
index 0000000..c0e34f6
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterPrecedenceConfigClassGetterSetterFieldConstructorTest.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.string;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbTypeAdapter;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * JsonbTypeAdapter on
+ *  - Field
+ *  - Constructor
+ *  - Getter
+ *  - Setter
+ *  - Config
+ *  - Class
+ *
+ *  Setter wins on read
+ *  Getter wins on write
+ *
+ *  Constructor adapter is called, but overwritten
+ */
+public class StringAdapterPrecedenceConfigClassGetterSetterFieldConstructorTest extends StringAdapterOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig().withAdapters(new Adapter.Config()));
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":\"test@domain.com\"}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:Setter.adaptFromJson}", actual.toString());
+        assertEquals("Constructor.adaptFromJson\n" +
+                "Contact.<init>\n" +
+                "Setter.adaptFromJson\n" +
+                "Contact.setEmail", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":\"test@domain.com:Getter.adaptToJson\"}", json);
+        assertEquals("Contact.getEmail\n" +
+                "Getter.adaptToJson", calls());
+    }
+
+    public static class Contact {
+
+        @JsonbTypeAdapter(Adapter.Field.class)
+        private Email email;
+
+        @JsonbCreator
+        public Contact(@JsonbProperty("email") @JsonbTypeAdapter(Adapter.Constructor.class) final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @JsonbTypeAdapter(Adapter.Getter.class)
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+        @JsonbTypeAdapter(Adapter.Setter.class)
+        public void setEmail(final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterPrecedenceConfigClassSetterFieldConstructorHasGetterTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterPrecedenceConfigClassSetterFieldConstructorHasGetterTest.java
new file mode 100644
index 0000000..6182694
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterPrecedenceConfigClassSetterFieldConstructorHasGetterTest.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.string;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbTypeAdapter;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * JsonbTypeAdapter on
+ *  - Field
+ *  - Constructor
+ *  - Setter
+ *  - Config
+ *  - Class
+ *
+ * Still has a getter
+ *
+ * Outcome
+ *  - Setter wins on read
+ *  - Field wins on write
+ *  - Constructor adapter is called, but overwritten
+ */
+public class StringAdapterPrecedenceConfigClassSetterFieldConstructorHasGetterTest extends StringAdapterOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig().withAdapters(new Adapter.Config()));
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":\"test@domain.com\"}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:Setter.adaptFromJson}", actual.toString());
+        assertEquals("Constructor.adaptFromJson\n" +
+                "Contact.<init>\n" +
+                "Setter.adaptFromJson\n" +
+                "Contact.setEmail", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":\"test@domain.com:Field.adaptToJson\"}", json);
+        assertEquals("Contact.getEmail\n" +
+                "Field.adaptToJson", calls());
+    }
+
+    public static class Contact {
+
+        @JsonbTypeAdapter(Adapter.Field.class)
+        private Email email;
+
+        @JsonbCreator
+        public Contact(@JsonbProperty("email") @JsonbTypeAdapter(Adapter.Constructor.class) final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+        @JsonbTypeAdapter(Adapter.Setter.class)
+        public void setEmail(final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterPrecedenceConfigClassTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterPrecedenceConfigClassTest.java
new file mode 100644
index 0000000..2bcea10
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/adapter/string/StringAdapterPrecedenceConfigClassTest.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.adapter.string;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * JsonbTypeAdapter on
+ *  - Config
+ *  - Class
+ *
+ * Has
+ *  - Getter
+ *  - Final Field
+ *
+ * Outcome:
+ *  - EmailClass wins on read
+ *  - EmailClass wins on write
+ *
+ * Question:
+ *  - Should Config win on read and write?
+ *    Adapters on the target type itself (Email) are effectively a hardcoded default adapter
+ *    If a user wishes to alter this behavior for a specific operation via the config, why
+ *    not let them?  This would be the most (only?) convenient way to change behavior without
+ *    sweeping code change.
+ */
+public class StringAdapterPrecedenceConfigClassTest extends StringAdapterOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig().withAdapters(new Adapter.Config()));
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":\"test@domain.com\"}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:EmailClass.adaptFromJson}", actual.toString());
+        assertEquals("Contact.<init>\n" +
+                "EmailClass.adaptFromJson\n" +
+                "Contact.setEmail", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact();
+        contact.setEmail(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":\"test@domain.com:EmailClass.adaptToJson\"}", json);
+        assertEquals("Contact.getEmail\n" +
+                "EmailClass.adaptToJson", calls());
+    }
+
+    public static class Contact {
+
+        private Email email;
+
+        public Contact() {
+            CALLS.called();
+        }
+
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+        public void setEmail(final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerOnClassDirectTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerOnClassDirectTest.java
new file mode 100644
index 0000000..b5138fa
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerOnClassDirectTest.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.serializer;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+
+import static org.junit.Assert.assertEquals;
+
+public class SerializerOnClassDirectTest extends SerializerOnClassTest {
+
+    public Jsonb jsonb() {
+        return JsonbBuilder.create();
+    }
+
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final String json = jsonb.toJson(email);
+        assertEquals("{\"user\":\"test\",\"domain\":\"domain.com\",\"call\":\"EmailClass.serialize\"}", json);
+        assertEquals("EmailClass.serialize", calls());
+    }
+
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"user\":\"test\",\"domain\":\"domain.com\"}";
+        final Email email = jsonb.fromJson(json, Email.class);
+        assertEquals("test@domain.com:EmailClass.deserialize", email.toString());
+        assertEquals("EmailClass.deserialize", calls());
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerOnClassSimpleTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerOnClassSimpleTest.java
new file mode 100644
index 0000000..ae62f0a
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerOnClassSimpleTest.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.serializer;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+
+import static org.junit.Assert.assertEquals;
+
+public class SerializerOnClassSimpleTest extends SerializerOnClassTest {
+
+    public Jsonb jsonb() {
+        return JsonbBuilder.create();
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\"}}";
+        final SerializerPrecedenceConfigClassTest.Contact actual = jsonb.fromJson(json, SerializerPrecedenceConfigClassTest.Contact.class);
+        assertEquals("Contact{email=test@domain.com:EmailClass.deserialize}", actual.toString());
+        assertEquals("Contact.<init>\n" +
+                "EmailClass.deserialize\n" +
+                "Contact.setEmail", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final SerializerPrecedenceConfigClassTest.Contact contact = new SerializerPrecedenceConfigClassTest.Contact();
+        contact.setEmail(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\",\"call\":\"EmailClass.serialize\"}}", json);
+        assertEquals("Contact.getEmail\n" +
+                "EmailClass.serialize", calls());
+    }
+
+    public static class Contact {
+
+        private Email email;
+
+        public Contact() {
+            CALLS.called();
+        }
+
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+        public void setEmail(final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerOnClassTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerOnClassTest.java
new file mode 100644
index 0000000..a008e8a
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerOnClassTest.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.serializer;
+
+import jakarta.json.bind.annotation.JsonbTypeDeserializer;
+import jakarta.json.bind.annotation.JsonbTypeSerializer;
+import jakarta.json.bind.serializer.DeserializationContext;
+import jakarta.json.bind.serializer.JsonbDeserializer;
+import jakarta.json.bind.serializer.JsonbSerializer;
+import jakarta.json.bind.serializer.SerializationContext;
+import jakarta.json.stream.JsonGenerator;
+import jakarta.json.stream.JsonParser;
+import org.apache.johnzon.jsonb.symmetry.Calls;
+import org.apache.johnzon.jsonb.symmetry.SymmetryTest;
+import org.junit.Before;
+
+import java.lang.reflect.Type;
+
+public abstract class SerializerOnClassTest extends SymmetryTest {
+
+    protected static final Calls CALLS = new Calls();
+
+    @Before
+    public void reset() {
+        CALLS.reset();
+    }
+
+    public static String calls() {
+        return CALLS.get();
+    }
+
+    @JsonbTypeDeserializer(Adapter.EmailClass.class)
+    @JsonbTypeSerializer(Adapter.EmailClass.class)
+    public static class Email {
+        final String user;
+        final String domain;
+        final String call;
+
+        public Email(final String user, final String domain) {
+            this(user, domain, null);
+        }
+
+        public Email(final String user, final String domain, final String call) {
+            this.user = user;
+            this.domain = domain;
+            this.call = call;
+        }
+
+        @Override
+        public String toString() {
+            if (call == null) {
+                return user + "@" + domain;
+            } else {
+                return user + "@" + domain + ":" + call;
+            }
+        }
+    }
+
+    public abstract static class Adapter implements JsonbSerializer<Email>, JsonbDeserializer<Email> {
+
+        @Override
+        public void serialize(final Email obj, final JsonGenerator generator, final SerializationContext ctx) {
+            final String call = CALLS.called(this);
+            generator.writeStartObject();
+            generator.write("user", obj.user);
+            generator.write("domain", obj.domain);
+            generator.write("call", call);
+            generator.writeEnd();
+        }
+
+        @Override
+        public Email deserialize(final JsonParser parser, final DeserializationContext ctx, final Type type) {
+            String user = null;
+            String domain = null;
+
+            while (parser.hasNext()) {
+                final JsonParser.Event event = parser.next();
+                if (event == JsonParser.Event.KEY_NAME) {
+                    final String key = parser.getString();
+                    parser.next();
+                    switch (key) {
+                        case "user":
+                            user = parser.getString();
+                            break;
+                        case "domain":
+                            domain = parser.getString();
+                            break;
+                        // skip "call"
+                        default: //ignore
+                    }
+                } else if (event == JsonParser.Event.END_OBJECT) {
+                    break;
+                }
+            }
+
+            final String call = CALLS.called(this);
+            return new Email(user, domain, call);
+        }
+
+        public static final class Getter extends Adapter {
+        }
+
+        public static final class Setter extends Adapter {
+        }
+
+        public static final class Field extends Adapter {
+        }
+
+        public static final class Constructor extends Adapter {
+        }
+
+        public static final class Config extends Adapter {
+        }
+
+        public static final class EmailClass extends Adapter {
+        }
+    }
+}
\ No newline at end of file
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceClassConstructorHasGetterFinalFieldTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceClassConstructorHasGetterFinalFieldTest.java
new file mode 100644
index 0000000..1c635c6
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceClassConstructorHasGetterFinalFieldTest.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.serializer;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbTypeDeserializer;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Adapter on
+ *  - Constructor
+ *  - Class
+ *
+ * Has
+ *  - Getter
+ *  - Final Field
+ *
+ * Outcome:
+ *  - Constructor wins on read
+ *  - Email wins on write
+ */
+public class SerializerPrecedenceClassConstructorHasGetterFinalFieldTest extends SerializerOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create();
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\"}}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:Constructor.deserialize}", actual.toString());
+        assertEquals("Constructor.deserialize\n" +
+                "Contact.<init>", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\",\"call\":\"EmailClass.serialize\"}}", json);
+        assertEquals("Contact.getEmail\n" +
+                "EmailClass.serialize", calls());
+    }
+
+    public static class Contact {
+
+        private final Email email;
+
+        @JsonbCreator
+        public Contact(@JsonbProperty("email") @JsonbTypeDeserializer(Adapter.Constructor.class) final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceClassConstructorHasGetterSetterTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceClassConstructorHasGetterSetterTest.java
new file mode 100644
index 0000000..ae2c519
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceClassConstructorHasGetterSetterTest.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.serializer;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbTypeDeserializer;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Adapters on
+ *  - Constructor
+ *  - Class
+ *
+ * Has
+ *  - Getter
+ *  - Setter
+ *
+ * Outcome:
+ *   - EmailClass wins on read
+ *   - EmailClass wins on write
+ *   - Constructor adapter is called, but overwritten
+ *
+ * Possible bug:
+ *  - Shouldn't the constructor win on read?
+ */
+public class SerializerPrecedenceClassConstructorHasGetterSetterTest extends SerializerOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create();
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\"}}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:EmailClass.deserialize}", actual.toString());
+        assertEquals("Constructor.deserialize\n" +
+                "Contact.<init>\n" +
+                "EmailClass.deserialize\n" +
+                "Contact.setEmail", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\",\"call\":\"EmailClass.serialize\"}}", json);
+        assertEquals("Contact.getEmail\n" +
+                "EmailClass.serialize", calls());
+    }
+
+    public static class Contact {
+
+        private Email email;
+
+        @JsonbCreator
+        public Contact(@JsonbProperty("email") @JsonbTypeDeserializer(Adapter.Constructor.class) final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+        public void setEmail(final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceConfigClassConstructorHasGetterFinalFieldTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceConfigClassConstructorHasGetterFinalFieldTest.java
new file mode 100644
index 0000000..234b037
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceConfigClassConstructorHasGetterFinalFieldTest.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.serializer;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbTypeDeserializer;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Adapter on
+ *  - Constructor
+ *  - Config
+ *  - Class
+ *
+ * Has
+ *  - Getter
+ *  - Final Field
+ *
+ * Outcome:
+ *  - Constructor wins on read
+ *  - Config wins on write
+ *
+ * Inconsistency:
+ *  - Equivalent test for JsonbTypeAdapter the EmailClass adapter wins on write (likely bug in JsonbTypeAdapter code)
+ */
+public class SerializerPrecedenceConfigClassConstructorHasGetterFinalFieldTest extends SerializerOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig()
+                .withSerializers(new Adapter.Config())
+                .withDeserializers(new Adapter.Config())
+        );
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\"}}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:Constructor.deserialize}", actual.toString());
+        assertEquals("Constructor.deserialize\n" +
+                "Contact.<init>", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\",\"call\":\"Config.serialize\"}}", json);
+        assertEquals("Contact.getEmail\n" +
+                "Config.serialize", calls());
+    }
+
+    public static class Contact {
+
+        private final Email email;
+
+        @JsonbCreator
+        public Contact(@JsonbProperty("email") @JsonbTypeDeserializer(Adapter.Constructor.class) final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceConfigClassConstructorHasGetterSetterTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceConfigClassConstructorHasGetterSetterTest.java
new file mode 100644
index 0000000..1783778
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceConfigClassConstructorHasGetterSetterTest.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.serializer;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbTypeDeserializer;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * JsonbTypeAdapter on
+ *  - Constructor
+ *  - Config
+ *  - Class
+ *
+ * Has
+ *  - Getter
+ *  - Setter
+ *
+ * Outcome:
+ *   - Config wins on read
+ *   - Config wins on write
+ *   - Constructor adapter is called, but overwritten
+ *
+ * Inconsistency:
+ *  - Equivalent test for JsonbTypeAdapter the EmailClass adapter wins (likely bug in JsonbTypeAdapter code)
+ *
+ * Possible bug:
+ *  - Shouldn't the constructor win on read?
+ */
+public class SerializerPrecedenceConfigClassConstructorHasGetterSetterTest extends SerializerOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig()
+                .withSerializers(new Adapter.Config())
+                .withDeserializers(new Adapter.Config())
+        );
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\"}}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:Config.deserialize}", actual.toString());
+        assertEquals("Constructor.deserialize\n" +
+                "Contact.<init>\n" +
+                "Config.deserialize\n" +
+                "Contact.setEmail", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\",\"call\":\"Config.serialize\"}}", json);
+        assertEquals("Contact.getEmail\n" +
+                "Config.serialize", calls());
+    }
+
+    public static class Contact {
+
+        private Email email;
+
+        @JsonbCreator
+        public Contact(@JsonbProperty("email") @JsonbTypeDeserializer(Adapter.Constructor.class) final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+        public void setEmail(final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceConfigClassDirectTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceConfigClassDirectTest.java
new file mode 100644
index 0000000..2f20e7c
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceConfigClassDirectTest.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.serializer;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * JsonbTypeAdapter on
+ *  - Config
+ *  - Class
+ *
+ *
+ * Outcome:
+ *  - Config wins on read
+ *  - Config wins on write
+ */
+public class SerializerPrecedenceConfigClassDirectTest extends SerializerOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig()
+                .withSerializers(new Adapter.Config())
+                .withDeserializers(new Adapter.Config())
+        );
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"user\":\"test\",\"domain\":\"domain.com\"}";
+        final Email actual = jsonb.fromJson(json, Email.class);
+        assertEquals("test@domain.com:Config.deserialize", actual.toString());
+        assertEquals("Config.deserialize", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+
+        final String json = jsonb.toJson(email);
+        assertEquals("{\"user\":\"test\",\"domain\":\"domain.com\",\"call\":\"Config.serialize\"}", json);
+        assertEquals("Config.serialize", calls());
+    }
+
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceConfigClassFieldConstructorHasGetterFinalFieldTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceConfigClassFieldConstructorHasGetterFinalFieldTest.java
new file mode 100644
index 0000000..7df7b86
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceConfigClassFieldConstructorHasGetterFinalFieldTest.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.serializer;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbTypeDeserializer;
+import jakarta.json.bind.annotation.JsonbTypeSerializer;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Adapter on
+ *  - Field
+ *  - Constructor
+ *  - Config
+ *  - Class
+ *
+ * Has
+ *  - Getter
+ *  - Final Field
+ *
+ *  Outcome:
+ *   - Constructor wins on read
+ *   - Field wins on write
+ */
+public class SerializerPrecedenceConfigClassFieldConstructorHasGetterFinalFieldTest extends SerializerOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig()
+                .withSerializers(new Adapter.Config())
+                .withDeserializers(new Adapter.Config())
+        );
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\"}}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:Constructor.deserialize}", actual.toString());
+        assertEquals("Constructor.deserialize\n" +
+                "Contact.<init>", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\",\"call\":\"Field.serialize\"}}", json);
+        assertEquals("Contact.getEmail\n" +
+                "Field.serialize", calls());
+    }
+
+    public static class Contact {
+
+        @JsonbTypeDeserializer(Adapter.Field.class)
+        @JsonbTypeSerializer(Adapter.Field.class)
+        private final Email email;
+
+        @JsonbCreator
+        public Contact(@JsonbProperty("email") @JsonbTypeDeserializer(Adapter.Constructor.class) final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceConfigClassFieldConstructorHasGetterSetterTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceConfigClassFieldConstructorHasGetterSetterTest.java
new file mode 100644
index 0000000..d48e6d7
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceConfigClassFieldConstructorHasGetterSetterTest.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.serializer;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbTypeDeserializer;
+import jakarta.json.bind.annotation.JsonbTypeSerializer;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Adapter on
+ *  - Field
+ *  - Constructor
+ *  - Config
+ *  - Class
+ *
+ * Has
+ *  - Getter
+ *  - Setter
+ *
+ *  Outcome:
+ *   - Field wins on read
+ *   - Field wins on write
+ *   - Constructor adapter is called, but overwritten
+ */
+public class SerializerPrecedenceConfigClassFieldConstructorHasGetterSetterTest extends SerializerOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig()
+                .withSerializers(new Adapter.Config())
+                .withDeserializers(new Adapter.Config())
+        );
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\"}}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:Field.deserialize}", actual.toString());
+        assertEquals("Constructor.deserialize\n" +
+                "Contact.<init>\n" +
+                "Field.deserialize\n" +
+                "Contact.setEmail", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\",\"call\":\"Field.serialize\"}}", json);
+        assertEquals("Contact.getEmail\n" +
+                "Field.serialize", calls());
+    }
+
+    public static class Contact {
+
+        @JsonbTypeDeserializer(Adapter.Field.class)
+        @JsonbTypeSerializer(Adapter.Field.class)
+        private Email email;
+
+        @JsonbCreator
+        public Contact(@JsonbProperty("email") @JsonbTypeDeserializer(Adapter.Constructor.class) final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+        public void setEmail(final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceConfigClassGetterFieldConstructorHasSetterTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceConfigClassGetterFieldConstructorHasSetterTest.java
new file mode 100644
index 0000000..a309d33
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceConfigClassGetterFieldConstructorHasSetterTest.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.serializer;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbTypeDeserializer;
+import jakarta.json.bind.annotation.JsonbTypeSerializer;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Adapter on
+ *  - Config
+ *  - Class
+ *  - Constructor
+ *  - Getter
+ *  - Field
+ *
+ * Still has a setter
+ *
+ * Outcome
+ *
+ *  - Field wins on read
+ *  - Getter wins on write
+ *  - Constructor adapter is called, but overwritten
+ */
+public class SerializerPrecedenceConfigClassGetterFieldConstructorHasSetterTest extends SerializerOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig()
+                .withSerializers(new Adapter.Config())
+                .withDeserializers(new Adapter.Config())
+        );
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\"}}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:Field.deserialize}", actual.toString());
+        assertEquals("Constructor.deserialize\n" +
+                "Contact.<init>\n" +
+                "Field.deserialize\n" +
+                "Contact.setEmail", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\",\"call\":\"Getter.serialize\"}}", json);
+        assertEquals("Contact.getEmail\n" +
+                "Getter.serialize", calls());
+    }
+
+    public static class Contact {
+
+        @JsonbTypeDeserializer(Adapter.Field.class)
+        @JsonbTypeSerializer(Adapter.Field.class)
+        private Email email;
+
+        @JsonbCreator
+        public Contact(@JsonbProperty("email") @JsonbTypeDeserializer(Adapter.Constructor.class) final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @JsonbTypeSerializer(Adapter.Getter.class)
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+        public void setEmail(final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceConfigClassGetterSetterFieldConstructorTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceConfigClassGetterSetterFieldConstructorTest.java
new file mode 100644
index 0000000..d906f21
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceConfigClassGetterSetterFieldConstructorTest.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.serializer;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbTypeDeserializer;
+import jakarta.json.bind.annotation.JsonbTypeSerializer;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Adapter on
+ *  - Field
+ *  - Constructor
+ *  - Getter
+ *  - Setter
+ *  - Config
+ *  - Class
+ *
+ *  Setter wins on read
+ *  Getter wins on write
+ *
+ *  Constructor adapter is called, but overwritten
+ */
+public class SerializerPrecedenceConfigClassGetterSetterFieldConstructorTest extends SerializerOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig()
+                .withSerializers(new Adapter.Config())
+                .withDeserializers(new Adapter.Config())
+        );
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\"}}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:Setter.deserialize}", actual.toString());
+        assertEquals("Constructor.deserialize\n" +
+                "Contact.<init>\n" +
+                "Setter.deserialize\n" +
+                "Contact.setEmail", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\",\"call\":\"Getter.serialize\"}}", json);
+        assertEquals("Contact.getEmail\n" +
+                "Getter.serialize", calls());
+    }
+
+    public static class Contact {
+
+        @JsonbTypeDeserializer(Adapter.Field.class)
+        @JsonbTypeSerializer(Adapter.Field.class)
+        private Email email;
+
+        @JsonbCreator
+        public Contact(@JsonbProperty("email") @JsonbTypeDeserializer(Adapter.Constructor.class) final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @JsonbTypeSerializer(Adapter.Getter.class)
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+        @JsonbTypeDeserializer(Adapter.Setter.class)
+        public void setEmail(final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceConfigClassSetterFieldConstructorHasGetterTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceConfigClassSetterFieldConstructorHasGetterTest.java
new file mode 100644
index 0000000..3ded7fa
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceConfigClassSetterFieldConstructorHasGetterTest.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.serializer;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbTypeDeserializer;
+import jakarta.json.bind.annotation.JsonbTypeSerializer;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Adapter on
+ *  - Field
+ *  - Constructor
+ *  - Setter
+ *  - Config
+ *  - Class
+ *
+ * Still has a getter
+ *
+ * Outcome
+ *  - Setter wins on read
+ *  - Field wins on write
+ *  - Constructor adapter is called, but overwritten
+ */
+public class SerializerPrecedenceConfigClassSetterFieldConstructorHasGetterTest extends SerializerOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig()
+                .withSerializers(new Adapter.Config())
+                .withDeserializers(new Adapter.Config())
+        );
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\"}}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:Setter.deserialize}", actual.toString());
+        assertEquals("Constructor.deserialize\n" +
+                "Contact.<init>\n" +
+                "Setter.deserialize\n" +
+                "Contact.setEmail", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\",\"call\":\"Field.serialize\"}}", json);
+        assertEquals("Contact.getEmail\n" +
+                "Field.serialize", calls());
+    }
+
+    public static class Contact {
+
+        @JsonbTypeDeserializer(Adapter.Field.class)
+        @JsonbTypeSerializer(Adapter.Field.class)
+        private Email email;
+
+        @JsonbCreator
+        public Contact(@JsonbProperty("email") @JsonbTypeDeserializer(Adapter.Constructor.class) final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+        @JsonbTypeDeserializer(Adapter.Setter.class)
+        public void setEmail(final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceConfigClassTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceConfigClassTest.java
new file mode 100644
index 0000000..768c673
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/symmetry/serializer/SerializerPrecedenceConfigClassTest.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.jsonb.symmetry.serializer;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Adapter on
+ *  - Config
+ *  - Class
+ *
+ * Has
+ *  - Getter
+ *  - Final Field
+ *
+ * Outcome:
+ *  - Config wins on read
+ *  - Config wins on write
+ *
+ * Inconsistency:
+ *  - Equivalent test for JsonbTypeAdapter the EmailClass adapter wins (likely bug in JsonbTypeAdapter code)
+ */
+public class SerializerPrecedenceConfigClassTest extends SerializerOnClassTest {
+
+    @Override
+    public Jsonb jsonb() {
+        return JsonbBuilder.create(new JsonbConfig()
+                .withSerializers(new Adapter.Config())
+                .withDeserializers(new Adapter.Config())
+        );
+    }
+
+    @Override
+    public void assertRead(final Jsonb jsonb) {
+        final String json = "{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\"}}";
+        final Contact actual = jsonb.fromJson(json, Contact.class);
+        assertEquals("Contact{email=test@domain.com:Config.deserialize}", actual.toString());
+        assertEquals("Contact.<init>\n" +
+                "Config.deserialize\n" +
+                "Contact.setEmail", calls());
+    }
+
+    @Override
+    public void assertWrite(final Jsonb jsonb) {
+        final Email email = new Email("test", "domain.com");
+        final Contact contact = new Contact();
+        contact.setEmail(email);
+        reset();
+
+        final String json = jsonb.toJson(contact);
+        assertEquals("{\"email\":{\"user\":\"test\",\"domain\":\"domain.com\",\"call\":\"Config.serialize\"}}", json);
+        assertEquals("Contact.getEmail\n" +
+                "Config.serialize", calls());
+    }
+
+    public static class Contact {
+
+        private Email email;
+
+        public Contact() {
+            CALLS.called();
+        }
+
+        public Email getEmail() {
+            CALLS.called();
+            return email;
+        }
+
+        public void setEmail(final Email email) {
+            CALLS.called();
+            this.email = email;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Contact{email=%s}", email);
+        }
+    }
+}