You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2023/01/06 03:02:49 UTC

[james-project] 10/12: JAMES-3771 Prevent SPI lookups on the HTTP eventloop

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

btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 49c358a39dad08f3c238716595d7795447152aa5
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Dec 15 13:59:24 2022 +0700

    JAMES-3771 Prevent SPI lookups on the HTTP eventloop
---
 .../james/backends/opensearch/ClientProvider.java  |   2 +-
 .../opensearch/json/JsonpDeserializerBase.java     | 397 +++++++++++++++++++++
 .../backends/opensearch/json/JsonpMapperBase.java  | 102 ++++++
 .../james/backends/opensearch/json/JsonpUtils.java | 207 +++++++++++
 .../json/UnexpectedJsonEventException.java         |  40 +++
 .../json/jackson/JacksonJsonProvider.java          | 298 ++++++++++++++++
 .../json/jackson/JacksonJsonpGenerator.java        | 374 +++++++++++++++++++
 .../json/jackson/JacksonJsonpLocation.java         |  54 +++
 .../json/jackson/JacksonJsonpMapper.java           | 123 +++++++
 .../json/jackson/JacksonJsonpParser.java           | 313 ++++++++++++++++
 .../opensearch/json/jackson/JacksonUtils.java      |  43 +++
 .../opensearch/json/jackson/JsonValueParser.java   | 108 ++++++
 .../backends/opensearch/json/package-info.java     |  29 ++
 13 files changed, 2089 insertions(+), 1 deletion(-)

diff --git a/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/ClientProvider.java b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/ClientProvider.java
index 89a104bb8b..abd0fc9945 100644
--- a/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/ClientProvider.java
+++ b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/ClientProvider.java
@@ -44,9 +44,9 @@ import org.apache.http.impl.client.BasicCredentialsProvider;
 import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
 import org.apache.http.ssl.SSLContextBuilder;
 import org.apache.http.ssl.TrustStrategy;
+import org.apache.james.backends.opensearch.json.jackson.JacksonJsonpMapper;
 import org.apache.james.util.concurrent.NamedThreadFactory;
 import org.opensearch.client.RestClient;
-import org.opensearch.client.json.jackson.JacksonJsonpMapper;
 import org.opensearch.client.opensearch.OpenSearchAsyncClient;
 import org.opensearch.client.transport.rest_client.RestClientTransport;
 import org.slf4j.Logger;
diff --git a/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/JsonpDeserializerBase.java b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/JsonpDeserializerBase.java
new file mode 100644
index 0000000000..c790e4ab2c
--- /dev/null
+++ b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/JsonpDeserializerBase.java
@@ -0,0 +1,397 @@
+/****************************************************************
+ * 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.james.backends.opensearch.json;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.opensearch.client.json.JsonpDeserializer;
+import org.opensearch.client.json.JsonpMapper;
+
+import jakarta.json.JsonNumber;
+import jakarta.json.JsonValue;
+import jakarta.json.stream.JsonParser;
+import jakarta.json.stream.JsonParser.Event;
+import jakarta.json.stream.JsonParsingException;
+
+/**
+ * Base class for {@link JsonpDeserializer} implementations that accept a set of JSON events known at instanciation time.
+ */
+public abstract class JsonpDeserializerBase<V> implements JsonpDeserializer<V> {
+
+    private final EnumSet<Event> acceptedEvents;
+    private final EnumSet<Event> nativeEvents;
+
+    protected JsonpDeserializerBase(EnumSet<Event> acceptedEvents) {
+        this(acceptedEvents, acceptedEvents);
+    }
+
+    protected JsonpDeserializerBase(EnumSet<Event> acceptedEvents, EnumSet<Event> nativeEvents) {
+        this.acceptedEvents = acceptedEvents;
+        this.nativeEvents = nativeEvents;
+    }
+
+    /** Combines accepted events from a number of deserializers */
+    protected static EnumSet<Event> allAcceptedEvents(JsonpDeserializer<?>... deserializers) {
+        EnumSet<Event> result = EnumSet.noneOf(Event.class);
+        for (JsonpDeserializer<?> deserializer: deserializers) {
+
+            EnumSet<Event> set = deserializer.acceptedEvents();
+            // Disabled for now. Only happens with the experimental Union2 and is caused by string and number
+            // parsers leniency. Need to be replaced with a check on a preferred event type.
+            //if (!Collections.disjoint(result, set)) {
+            //    throw new IllegalArgumentException("Deserializer accepted events are not disjoint");
+            //}
+
+            result.addAll(set);
+        }
+        return result;
+    }
+
+    @Override
+    public EnumSet<Event> nativeEvents() {
+        return nativeEvents;
+    }
+
+    /**
+     * The JSON events this deserializer accepts as a starting point
+     */
+    public final EnumSet<Event> acceptedEvents() {
+        return acceptedEvents;
+    }
+
+    /**
+     * Convenience method for {@code acceptedEvents.contains(event)}
+     */
+    public final boolean accepts(Event event) {
+        return acceptedEvents.contains(event);
+    }
+
+    //---------------------------------------------------------------------------------------------
+
+    //----- Builtin types
+
+    static final JsonpDeserializer<String> STRING =
+        // String parsing is lenient and accepts any other primitive type
+        new JsonpDeserializerBase<String>(EnumSet.of(
+                Event.KEY_NAME, Event.VALUE_STRING, Event.VALUE_NUMBER,
+                Event.VALUE_FALSE, Event.VALUE_TRUE
+            ),
+            EnumSet.of(Event.VALUE_STRING)
+        ) {
+            @Override
+            public String deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+                if (event == Event.VALUE_TRUE) {
+                    return "true";
+                }
+                if (event == Event.VALUE_FALSE) {
+                    return "false";
+                }
+                return parser.getString(); // also accepts numbers
+            }
+        };
+
+    static final JsonpDeserializer<Integer> INTEGER =
+        new JsonpDeserializerBase<Integer>(
+            EnumSet.of(Event.VALUE_NUMBER, Event.VALUE_STRING),
+            EnumSet.of(Event.VALUE_NUMBER)
+        ) {
+            @Override
+            public Integer deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+                if (event == Event.VALUE_STRING) {
+                    return Integer.valueOf(parser.getString());
+                }
+                return parser.getInt();
+            }
+        };
+
+    static final JsonpDeserializer<Boolean> BOOLEAN =
+        new JsonpDeserializerBase<Boolean>(
+            EnumSet.of(Event.VALUE_FALSE, Event.VALUE_TRUE, Event.VALUE_STRING),
+            EnumSet.of(Event.VALUE_FALSE, Event.VALUE_TRUE)
+        ) {
+            @Override
+            public Boolean deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+                if (event == Event.VALUE_STRING) {
+                    return Boolean.parseBoolean(parser.getString());
+                } else {
+                    return event == Event.VALUE_TRUE;
+                }
+            }
+        };
+
+    static final JsonpDeserializer<Long> LONG =
+        new JsonpDeserializerBase<Long>(
+            EnumSet.of(Event.VALUE_NUMBER, Event.VALUE_STRING),
+            EnumSet.of(Event.VALUE_NUMBER)
+        ) {
+            @Override
+            public Long deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+                if (event == Event.VALUE_STRING) {
+                    return Long.valueOf(parser.getString());
+                }
+                return parser.getLong();
+            }
+        };
+
+    static final JsonpDeserializer<Float> FLOAT =
+        new JsonpDeserializerBase<Float>(
+            EnumSet.of(Event.VALUE_NUMBER, Event.VALUE_STRING),
+            EnumSet.of(Event.VALUE_NUMBER)
+
+        ) {
+            @Override
+            public Float deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+                if (event == Event.VALUE_STRING) {
+                    return Float.valueOf(parser.getString());
+                }
+                return parser.getBigDecimal().floatValue();
+            }
+        };
+
+    static final JsonpDeserializer<Double> DOUBLE =
+        new JsonpDeserializerBase<Double>(
+            EnumSet.of(Event.VALUE_NUMBER, Event.VALUE_STRING),
+            EnumSet.of(Event.VALUE_NUMBER)
+        ) {
+            @Override
+            public Double deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+                if (event == Event.VALUE_STRING) {
+                    return Double.valueOf(parser.getString());
+                }
+                return parser.getBigDecimal().doubleValue();
+            }
+        };
+
+    static final class DoubleOrNullDeserializer extends JsonpDeserializerBase<Double> {
+        static final EnumSet<Event> nativeEvents = EnumSet.of(Event.VALUE_NUMBER, Event.VALUE_NULL);
+        static final EnumSet<Event> acceptedEvents = EnumSet.of(Event.VALUE_STRING, Event.VALUE_NUMBER, Event.VALUE_NULL);
+        private final double defaultValue;
+
+        DoubleOrNullDeserializer(double defaultValue) {
+            super(acceptedEvents, nativeEvents);
+            this.defaultValue = defaultValue;
+        }
+
+        @Override
+        public Double deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+            if (event == Event.VALUE_NULL) {
+                return defaultValue;
+            }
+            if (event == Event.VALUE_STRING) {
+                return Double.valueOf(parser.getString());
+            }
+            return parser.getBigDecimal().doubleValue();
+        }
+    }
+
+    static final class IntOrNullDeserializer extends JsonpDeserializerBase<Integer> {
+        static final EnumSet<Event> nativeEvents = EnumSet.of(Event.VALUE_NUMBER, Event.VALUE_NULL);
+        static final EnumSet<Event> acceptedEvents = EnumSet.of(Event.VALUE_STRING, Event.VALUE_NUMBER, Event.VALUE_NULL);
+        private final int defaultValue;
+
+        IntOrNullDeserializer(int defaultValue) {
+            super(acceptedEvents, nativeEvents);
+            this.defaultValue = defaultValue;
+        }
+
+        @Override
+        public Integer deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+            if (event == Event.VALUE_NULL) {
+                return defaultValue;
+            }
+            if (event == Event.VALUE_STRING) {
+                return Integer.valueOf(parser.getString());
+            }
+            return parser.getInt();
+        }
+    }
+
+    static final class StringOrNullDeserializer extends JsonpDeserializerBase<String> {
+        static final EnumSet<Event> nativeEvents = EnumSet.of(Event.VALUE_STRING, Event.VALUE_NULL);
+        static final EnumSet<Event> acceptedEvents = EnumSet.of(Event.KEY_NAME, Event.VALUE_STRING,
+            Event.VALUE_NUMBER, Event.VALUE_FALSE, Event.VALUE_TRUE, Event.VALUE_NULL);
+
+        StringOrNullDeserializer() {
+            super(acceptedEvents, nativeEvents);
+        }
+
+        @Override
+        public String deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+            if (event == Event.VALUE_NULL) {
+                return null;
+            }
+            if (event == Event.VALUE_TRUE) {
+                return "true";
+            }
+            if (event == Event.VALUE_FALSE) {
+                return "false";
+            }
+            return parser.getString();
+        }
+    }
+
+    static final JsonpDeserializer<Double> DOUBLE_OR_NAN =
+        new JsonpDeserializerBase<Double>(
+            EnumSet.of(Event.VALUE_NUMBER, Event.VALUE_STRING, Event.VALUE_NULL),
+            EnumSet.of(Event.VALUE_NUMBER, Event.VALUE_NULL)
+        ) {
+            @Override
+            public Double deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+                if (event == Event.VALUE_NULL) {
+                    return Double.NaN;
+                }
+                if (event == Event.VALUE_STRING) {
+                    return Double.valueOf(parser.getString());
+                }
+                return parser.getBigDecimal().doubleValue();
+            }
+        };
+
+    static final JsonpDeserializer<Number> NUMBER =
+        new JsonpDeserializerBase<Number>(
+            EnumSet.of(Event.VALUE_NUMBER, Event.VALUE_STRING),
+            EnumSet.of(Event.VALUE_NUMBER)
+        ) {
+            @Override
+            public Number deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+                if (event == Event.VALUE_STRING) {
+                    return Double.valueOf(parser.getString());
+                }
+                return ((JsonNumber)parser.getValue()).numberValue();
+            }
+        };
+
+    static final JsonpDeserializer<JsonValue> JSON_VALUE =
+        new JsonpDeserializerBase<JsonValue>(
+            EnumSet.allOf(Event.class)
+        ) {
+            @Override
+            public JsonValue deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+                return parser.getValue();
+            }
+        };
+
+    static final JsonpDeserializer<Void> VOID = new JsonpDeserializerBase<Void>(
+        EnumSet.noneOf(Event.class)
+    ) {
+        @Override
+        public Void deserialize(JsonParser parser, JsonpMapper mapper) {
+            throw new JsonParsingException("Void types should not have any value", parser.getLocation());
+        }
+
+        @Override
+        public Void deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+            return deserialize(parser, mapper);
+        }
+    };
+
+    //----- Collections
+
+    static class ArrayDeserializer<T> implements JsonpDeserializer<List<T>> {
+        private final JsonpDeserializer<T> itemDeserializer;
+        private EnumSet<Event> acceptedEvents;
+        private static final EnumSet<Event> nativeEvents = EnumSet.of(Event.START_ARRAY);
+
+        protected ArrayDeserializer(JsonpDeserializer<T> itemDeserializer) {
+            this.itemDeserializer = itemDeserializer;
+        }
+
+        @Override
+        public EnumSet<Event> nativeEvents() {
+            return nativeEvents;
+        }
+
+        @Override
+        public EnumSet<Event> acceptedEvents() {
+            // Accepted events is computed lazily
+            // no need for double-checked lock, we don't care about computing it several times
+            if (acceptedEvents == null) {
+                acceptedEvents = EnumSet.of(Event.START_ARRAY);
+                acceptedEvents.addAll(itemDeserializer.acceptedEvents());
+            }
+            return acceptedEvents;
+        }
+
+        @Override
+        public List<T> deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+            if (event == Event.START_ARRAY) {
+                List<T> result = new ArrayList<>();
+                while ((event = parser.next()) != Event.END_ARRAY) {
+                    JsonpUtils.ensureAccepts(itemDeserializer, parser, event);
+                    result.add(itemDeserializer.deserialize(parser, mapper, event));
+                }
+                return result;
+            } else {
+                // Single-value mode
+                JsonpUtils.ensureAccepts(itemDeserializer, parser, event);
+                return Collections.singletonList(itemDeserializer.deserialize(parser, mapper, event));
+            }
+        }
+    }
+
+    static class StringMapDeserializer<T> extends JsonpDeserializerBase<Map<String, T>> {
+        private final JsonpDeserializer<T> itemDeserializer;
+
+        protected StringMapDeserializer(JsonpDeserializer<T> itemDeserializer) {
+            super(EnumSet.of(Event.START_OBJECT));
+            this.itemDeserializer = itemDeserializer;
+        }
+
+        @Override
+        public Map<String, T> deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+            Map<String, T> result = new HashMap<>();
+            while ((event = parser.next()) != Event.END_OBJECT) {
+                JsonpUtils.expectEvent(parser, Event.KEY_NAME, event);
+                String key = parser.getString();
+                T value = itemDeserializer.deserialize(parser, mapper);
+                result.put(key, value);
+            }
+            return result;
+        }
+    }
+
+    static class EnumMapDeserializer<K, V> extends JsonpDeserializerBase<Map<K, V>> {
+        private final JsonpDeserializer<K> keyDeserializer;
+        private final JsonpDeserializer<V> valueDeserializer;
+
+        protected EnumMapDeserializer(JsonpDeserializer<K> keyDeserializer, JsonpDeserializer<V> valueDeserializer) {
+            super(EnumSet.of(Event.START_OBJECT));
+            this.keyDeserializer = keyDeserializer;
+            this.valueDeserializer = valueDeserializer;
+        }
+
+        @Override
+        public Map<K, V> deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+            Map<K, V> result = new HashMap<>();
+            while ((event = parser.next()) != Event.END_OBJECT) {
+                JsonpUtils.expectEvent(parser, Event.KEY_NAME, event);
+                K key = keyDeserializer.deserialize(parser, mapper, event);
+                V value = valueDeserializer.deserialize(parser, mapper);
+                result.put(key, value);
+            }
+            return result;
+        }
+    }
+}
diff --git a/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/JsonpMapperBase.java b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/JsonpMapperBase.java
new file mode 100644
index 0000000000..5a710795b8
--- /dev/null
+++ b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/JsonpMapperBase.java
@@ -0,0 +1,102 @@
+/****************************************************************
+ * 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.james.backends.opensearch.json;
+
+import java.lang.reflect.Field;
+
+import javax.annotation.Nullable;
+
+import org.opensearch.client.json.JsonpDeserializable;
+import org.opensearch.client.json.JsonpDeserializer;
+import org.opensearch.client.json.JsonpMapper;
+import org.opensearch.client.json.JsonpSerializable;
+import org.opensearch.client.json.JsonpSerializer;
+
+import jakarta.json.JsonValue;
+import jakarta.json.stream.JsonGenerator;
+import jakarta.json.stream.JsonParser;
+
+public abstract class JsonpMapperBase implements JsonpMapper {
+
+    /** Get a serializer when none of the builtin ones are applicable */
+    protected abstract <T> JsonpDeserializer<T> getDefaultDeserializer(Class<T> clazz);
+
+    @Override
+    public <T> T deserialize(JsonParser parser, Class<T> clazz) {
+        JsonpDeserializer<T> deserializer = findDeserializer(clazz);
+        if (deserializer != null) {
+            return deserializer.deserialize(parser, this);
+        }
+
+        return getDefaultDeserializer(clazz).deserialize(parser, this);
+    }
+
+    @Nullable
+    @SuppressWarnings("unchecked")
+    public static <T> JsonpDeserializer<T> findDeserializer(Class<T> clazz) {
+        JsonpDeserializable annotation = clazz.getAnnotation(JsonpDeserializable.class);
+        if (annotation != null) {
+            try {
+                Field field = clazz.getDeclaredField(annotation.field());
+                return (JsonpDeserializer<T>)field.get(null);
+            } catch (Exception e) {
+                throw new RuntimeException("No deserializer found in '" + clazz.getName() + "." + annotation.field() + "'");
+            }
+        }
+
+        return null;
+    }
+
+    @Nullable
+    @SuppressWarnings("unchecked")
+    public static <T> JsonpSerializer<T> findSerializer(T value) {
+        Class<?> valueClass = value.getClass();
+        if (JsonpSerializable.class.isAssignableFrom(valueClass)) {
+            return (JsonpSerializer<T>) JsonpSerializableSerializer.INSTANCE;
+        }
+
+        if (JsonValue.class.isAssignableFrom(valueClass)) {
+            return (JsonpSerializer<T>) JsonpValueSerializer.INSTANCE;
+        }
+
+        return null;
+    }
+
+    protected static class JsonpSerializableSerializer<T extends JsonpSerializable> implements JsonpSerializer<T> {
+        @Override
+        public void serialize(T value, JsonGenerator generator, JsonpMapper mapper) {
+            value.serialize(generator, mapper);
+        }
+
+        protected static final JsonpSerializer<?> INSTANCE = new JsonpSerializableSerializer<>();
+
+    }
+
+    protected static class JsonpValueSerializer implements JsonpSerializer<JsonValue> {
+        @Override
+        public void serialize(JsonValue value, JsonGenerator generator, JsonpMapper mapper) {
+            generator.write(value);
+        }
+
+        protected static final JsonpSerializer<?> INSTANCE = new JsonpValueSerializer();
+
+    }
+
+}
diff --git a/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/JsonpUtils.java b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/JsonpUtils.java
new file mode 100644
index 0000000000..5694a5cbee
--- /dev/null
+++ b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/JsonpUtils.java
@@ -0,0 +1,207 @@
+/****************************************************************
+ * 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.james.backends.opensearch.json;
+
+import java.io.StringReader;
+import java.util.AbstractMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nullable;
+
+import org.opensearch.client.json.JsonpDeserializer;
+import org.opensearch.client.json.JsonpMapper;
+import org.opensearch.client.json.JsonpSerializable;
+import org.opensearch.client.json.JsonpSerializer;
+import org.opensearch.client.util.ObjectBuilder;
+
+import jakarta.json.JsonObject;
+import jakarta.json.JsonString;
+import jakarta.json.JsonValue;
+import jakarta.json.stream.JsonGenerator;
+import jakarta.json.stream.JsonParser;
+import jakarta.json.stream.JsonParser.Event;
+import jakarta.json.stream.JsonParsingException;
+
+public class JsonpUtils {
+
+    /**
+     * Advances the parser to the next event and checks that this even is the expected one.
+     *
+     * @return the expected event
+     *
+     * @throws jakarta.json.JsonException if an i/o error occurs (IOException would be cause of JsonException)
+     * @throws JsonParsingException if the event is not the expected one, or if the parser encounters invalid
+     *         JSON when advancing to next state.
+     * @throws java.util.NoSuchElementException if there are no more parsing states.
+     */
+    public static Event expectNextEvent(JsonParser parser, Event expected) {
+        Event event = parser.next();
+        expectEvent(parser, expected, event);
+        return event;
+    }
+
+    public static void expectEvent(JsonParser parser, Event expected, Event event) {
+        if (event != expected) {
+            throw new UnexpectedJsonEventException(parser, event, expected);
+        }
+    }
+
+    public static String expectKeyName(JsonParser parser, Event event) {
+        JsonpUtils.expectEvent(parser, Event.KEY_NAME, event);
+        return parser.getString();
+    }
+
+    public static void ensureAccepts(JsonpDeserializer<?> deserializer, JsonParser parser, Event event) {
+        if (!deserializer.acceptedEvents().contains(event)) {
+            throw new UnexpectedJsonEventException(parser, event, deserializer.acceptedEvents());
+        }
+    }
+
+    /**
+     * Skip the value at the next position of the parser.
+     */
+    public static void skipValue(JsonParser parser) {
+        skipValue(parser, parser.next());
+    }
+
+    /**
+     * Skip the value at the current position of the parser.
+     */
+    public static void skipValue(JsonParser parser, Event event) {
+        switch (event) {
+            case START_OBJECT:
+                parser.skipObject();
+                break;
+
+            case START_ARRAY:
+                parser.skipArray();
+                break;
+
+            default:
+                // Not a structure, no additional skipping needed
+                break;
+        }
+    }
+
+    public static <T> T buildVariant(JsonParser parser, ObjectBuilder<T> builder) {
+        if (builder == null) {
+            throw new JsonParsingException("No variant found", parser.getLocation());
+        }
+        return builder.build();
+    }
+
+    public static <T> void serialize(T value, JsonGenerator generator, @Nullable JsonpSerializer<T> serializer, JsonpMapper mapper) {
+        if (serializer != null) {
+            serializer.serialize(value, generator, mapper);
+        } else if (value instanceof JsonpSerializable) {
+            ((JsonpSerializable) value).serialize(generator, mapper);
+        } else {
+            mapper.serialize(value, generator);
+        }
+    }
+
+    /**
+     * Looks ahead a field value in the Json object from the upcoming object in a parser, which should be on the
+     * START_OBJECT event.
+     *
+     * Returns a pair containing that value and a parser that should be used to actually parse the object
+     * (the object has been consumed from the original one).
+     */
+    public static Map.Entry<String, JsonParser> lookAheadFieldValue(
+        String name, String defaultValue, JsonParser parser, JsonpMapper mapper
+    ) {
+        // FIXME: need a buffering parser wrapper so that we don't roundtrip through a JsonObject and a String
+        // FIXME: resulting parser should return locations that are offset with the original parser's location
+        JsonObject object = parser.getObject();
+        String result = object.getString(name, null);
+
+        if (result == null) {
+            result = defaultValue;
+        }
+
+        if (result == null) {
+            throw new JsonParsingException("Property '" + name + "' not found", parser.getLocation());
+        }
+
+        return new AbstractMap.SimpleImmutableEntry<>(result, objectParser(object, mapper));
+    }
+
+    /**
+     * Create a parser that traverses a JSON object
+     */
+    public static JsonParser objectParser(JsonObject object, JsonpMapper mapper) {
+        // FIXME: we should have used createParser(object), but this doesn't work as it creates a
+        // org.glassfish.json.JsonStructureParser that doesn't implement the JsonP 1.0.1 features, in particular
+        // parser.getObject(). So deserializing recursive internally-tagged union would fail with UnsupportedOperationException
+        // While glassfish has this issue or until we write our own, we roundtrip through a string.
+
+        String strObject = object.toString();
+        return mapper.jsonProvider().createParser(new StringReader(strObject));
+    }
+
+    public static String toString(JsonValue value) {
+        switch (value.getValueType()) {
+            case OBJECT:
+                throw new IllegalArgumentException("Json objects cannot be used as string");
+
+            case ARRAY:
+                return value.asJsonArray().stream()
+                    .map(JsonpUtils::toString)
+                    .collect(Collectors.joining(","));
+
+            case STRING:
+                return ((JsonString)value).getString();
+
+            case TRUE:
+                return "true";
+
+            case FALSE:
+                return "false";
+
+            case NULL:
+                return "null";
+
+            case NUMBER:
+                return value.toString();
+
+            default:
+                throw new IllegalArgumentException("Unknown JSON value type: '" + value + "'");
+        }
+    }
+
+    public static void serializeDoubleOrNull(JsonGenerator generator, double value, double defaultValue) {
+        // Only output null if the default value isn't finite, which cannot be represented as JSON
+        if (value == defaultValue && !Double.isFinite(defaultValue)) {
+            generator.writeNull();
+        } else {
+            generator.write(value);
+        }
+    }
+
+    public static void serializeIntOrNull(JsonGenerator generator, int value, int defaultValue) {
+        // Only output null if the default value isn't finite, which cannot be represented as JSON
+        if (value == defaultValue && defaultValue == Integer.MAX_VALUE || defaultValue == Integer.MIN_VALUE) {
+            generator.writeNull();
+        } else {
+            generator.write(value);
+        }
+    }
+}
diff --git a/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/UnexpectedJsonEventException.java b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/UnexpectedJsonEventException.java
new file mode 100644
index 0000000000..faf7d5fe85
--- /dev/null
+++ b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/UnexpectedJsonEventException.java
@@ -0,0 +1,40 @@
+/****************************************************************
+ * 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.james.backends.opensearch.json;
+
+import java.util.EnumSet;
+
+import jakarta.json.stream.JsonParser;
+import jakarta.json.stream.JsonParser.Event;
+import jakarta.json.stream.JsonParsingException;
+
+public class UnexpectedJsonEventException extends JsonParsingException {
+    public UnexpectedJsonEventException(JsonParser parser, Event event) {
+        super("Unexpected JSON event '" + event + "'", parser.getLocation());
+    }
+
+    public UnexpectedJsonEventException(JsonParser parser, Event event, Event expected) {
+        super("Unexpected JSON event '" + event + "' instead of '" + expected + "'", parser.getLocation());
+    }
+
+    public UnexpectedJsonEventException(JsonParser parser, Event event, EnumSet<Event> expected) {
+        super("Unexpected JSON event '" + event + "' instead of '" + expected + "'", parser.getLocation());
+    }
+}
diff --git a/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonJsonProvider.java b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonJsonProvider.java
new file mode 100644
index 0000000000..e7d128977f
--- /dev/null
+++ b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonJsonProvider.java
@@ -0,0 +1,298 @@
+/****************************************************************
+ * 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.james.backends.opensearch.json.jackson;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.nio.charset.Charset;
+import java.util.Collections;
+import java.util.Map;
+
+import com.fasterxml.jackson.core.JsonFactory;
+
+import jakarta.json.JsonArray;
+import jakarta.json.JsonArrayBuilder;
+import jakarta.json.JsonBuilderFactory;
+import jakarta.json.JsonObject;
+import jakarta.json.JsonObjectBuilder;
+import jakarta.json.JsonReader;
+import jakarta.json.JsonReaderFactory;
+import jakarta.json.JsonWriter;
+import jakarta.json.JsonWriterFactory;
+import jakarta.json.spi.JsonProvider;
+import jakarta.json.stream.JsonGenerator;
+import jakarta.json.stream.JsonGeneratorFactory;
+import jakarta.json.stream.JsonParser;
+import jakarta.json.stream.JsonParserFactory;
+
+/**
+ * A partial implementation of JSONP's SPI on top of Jackson.
+ */
+public class JacksonJsonProvider extends JsonProvider {
+
+    private final JsonFactory jsonFactory;
+
+    public JacksonJsonProvider(JsonFactory jsonFactory) {
+        this.jsonFactory = jsonFactory;
+    }
+
+    public JacksonJsonProvider() {
+        this(new JsonFactory());
+    }
+
+    /**
+     * Return the underlying Jackson {@link JsonFactory}.
+     */
+    public JsonFactory jacksonJsonFactory() {
+        return this.jsonFactory;
+    }
+
+    //---------------------------------------------------------------------------------------------
+    // Parser
+
+    private final ParserFactory defaultParserFactory = new ParserFactory(null);
+
+    @Override
+    public JsonParserFactory createParserFactory(Map<String, ?> config) {
+        if (config == null || config.isEmpty()) {
+            return defaultParserFactory;
+        } else {
+            // TODO: handle specific configuration
+            return defaultParserFactory;
+        }
+    }
+
+    @Override
+    public JsonParser createParser(Reader reader) {
+        return defaultParserFactory.createParser(reader);
+    }
+
+    @Override
+    public JsonParser createParser(InputStream in) {
+        return defaultParserFactory.createParser(in);
+    }
+
+    private class ParserFactory implements JsonParserFactory {
+
+        private final Map<String, ?> config;
+
+        ParserFactory(Map<String, ?> config) {
+            this.config = config == null ? Collections.emptyMap() : config;
+        }
+
+        @Override
+        public JsonParser createParser(Reader reader) {
+            try {
+                return new JacksonJsonpParser(jsonFactory.createParser(reader));
+            } catch (IOException ioe) {
+                throw JacksonUtils.convertException(ioe);
+            }
+        }
+
+        @Override
+        public JsonParser createParser(InputStream in) {
+            try {
+                return new JacksonJsonpParser(jsonFactory.createParser(in));
+            } catch (IOException ioe) {
+                throw JacksonUtils.convertException(ioe);
+            }
+        }
+
+        @Override
+        public JsonParser createParser(InputStream in, Charset charset) {
+            try {
+                return new JacksonJsonpParser(jsonFactory.createParser(new InputStreamReader(in, charset)));
+            } catch (IOException ioe) {
+                throw JacksonUtils.convertException(ioe);
+            }
+        }
+
+        /**
+         * Not implemented.
+         */
+        @Override
+        public JsonParser createParser(JsonObject obj) {
+            return JsonProvider.provider().createParserFactory(null).createParser(obj);
+        }
+
+        /**
+         * Not implemented.
+         */
+        @Override
+        public JsonParser createParser(JsonArray array) {
+            return JsonProvider.provider().createParserFactory(null).createParser(array);
+        }
+
+        /**
+         * Not implemented.
+         */
+        @Override
+        public Map<String, ?> getConfigInUse() {
+            return config;
+        }
+    }
+
+    //---------------------------------------------------------------------------------------------
+    // Generator
+
+    private final JsonGeneratorFactory defaultGeneratorFactory = new GeneratorFactory(null);
+
+    @Override
+    public JsonGeneratorFactory createGeneratorFactory(Map<String, ?> config) {
+        if (config == null || config.isEmpty()) {
+            return defaultGeneratorFactory;
+        } else {
+            // TODO: handle specific configuration
+            return defaultGeneratorFactory;
+        }
+    }
+
+    @Override
+    public JsonGenerator createGenerator(Writer writer) {
+        return defaultGeneratorFactory.createGenerator(writer);
+    }
+
+    @Override
+    public JsonGenerator createGenerator(OutputStream out) {
+        return defaultGeneratorFactory.createGenerator(out);
+    }
+
+    private class GeneratorFactory implements JsonGeneratorFactory {
+
+        private final Map<String, ?> config;
+
+        GeneratorFactory(Map<String, ?> config) {
+            this.config = config == null ? Collections.emptyMap() : config;
+        }
+
+        @Override
+        public JsonGenerator createGenerator(Writer writer) {
+            try {
+                return new JacksonJsonpGenerator(jsonFactory.createGenerator(writer));
+            } catch (IOException ioe) {
+                throw JacksonUtils.convertException(ioe);
+            }
+        }
+
+        @Override
+        public JsonGenerator createGenerator(OutputStream out) {
+            try {
+                return new JacksonJsonpGenerator(jsonFactory.createGenerator(out));
+            } catch (IOException ioe) {
+                throw JacksonUtils.convertException(ioe);
+            }
+        }
+
+        @Override
+        public JsonGenerator createGenerator(OutputStream out, Charset charset) {
+            try {
+                return new JacksonJsonpGenerator(jsonFactory.createGenerator(new OutputStreamWriter(out, charset)));
+            } catch (IOException ioe) {
+                throw JacksonUtils.convertException(ioe);
+            }
+
+        }
+
+        @Override
+        public Map<String, ?> getConfigInUse() {
+            return config;
+        }
+    }
+
+    //---------------------------------------------------------------------------------------------
+    // Unsupported operations
+
+    /**
+     * Not implemented.
+     */
+    @Override
+    public JsonReader createReader(Reader reader) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Not implemented.
+     */
+    @Override
+    public JsonReader createReader(InputStream in) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Not implemented.
+     */
+    @Override
+    public JsonWriter createWriter(Writer writer) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Not implemented.
+     */
+    @Override
+    public JsonWriter createWriter(OutputStream out) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Not implemented.
+     */
+    @Override
+    public JsonWriterFactory createWriterFactory(Map<String, ?> config) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Not implemented.
+     */
+    @Override
+    public JsonReaderFactory createReaderFactory(Map<String, ?> config) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Not implemented.
+     */
+    @Override
+    public JsonObjectBuilder createObjectBuilder() {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Not implemented.
+     */
+    @Override
+    public JsonArrayBuilder createArrayBuilder() {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Not implemented.
+     */
+    @Override
+    public JsonBuilderFactory createBuilderFactory(Map<String, ?> config) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonJsonpGenerator.java b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonJsonpGenerator.java
new file mode 100644
index 0000000000..c8758d11d8
--- /dev/null
+++ b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonJsonpGenerator.java
@@ -0,0 +1,374 @@
+/****************************************************************
+ * 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.james.backends.opensearch.json.jackson;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Map;
+
+import com.fasterxml.jackson.core.JsonStreamContext;
+
+import jakarta.json.JsonNumber;
+import jakarta.json.JsonString;
+import jakarta.json.JsonValue;
+import jakarta.json.stream.JsonGenerationException;
+import jakarta.json.stream.JsonGenerator;
+
+/**
+ * A JSONP generator implementation on top of Jackson.
+ */
+public class JacksonJsonpGenerator implements JsonGenerator {
+
+    private final com.fasterxml.jackson.core.JsonGenerator generator;
+
+    public JacksonJsonpGenerator(com.fasterxml.jackson.core.JsonGenerator generator) {
+        this.generator = generator;
+    }
+
+    /**
+     * Returns the underlying Jackson generator.
+     */
+    public com.fasterxml.jackson.core.JsonGenerator jacksonGenerator() {
+        return generator;
+    }
+
+    @Override
+    public JsonGenerator writeStartObject() {
+        try {
+            generator.writeStartObject();
+        } catch (IOException e) {
+            throw JacksonUtils.convertException(e);
+        }
+        return this;
+    }
+
+    @Override
+    public JsonGenerator writeStartObject(String name) {
+        try {
+            generator.writeFieldName(name);
+            generator.writeStartObject();
+        } catch (IOException e) {
+            throw JacksonUtils.convertException(e);
+        }
+        return this;
+    }
+
+    @Override
+    public JsonGenerator writeStartArray() {
+        try {
+            generator.writeStartArray();
+        } catch (IOException e) {
+            throw JacksonUtils.convertException(e);
+        }
+        return this;
+    }
+
+    @Override
+    public JsonGenerator writeStartArray(String name) {
+        try {
+            generator.writeFieldName(name);
+            generator.writeStartArray();
+        } catch (IOException e) {
+            throw JacksonUtils.convertException(e);
+        }
+        return this;
+    }
+
+    @Override
+    public JsonGenerator writeKey(String name) {
+        try {
+            generator.writeFieldName(name);
+        } catch (IOException e) {
+            throw JacksonUtils.convertException(e);
+        }
+        return this;
+    }
+
+    @Override
+    public JsonGenerator write(String name, JsonValue value) {
+        try {
+            generator.writeFieldName(name);
+            writeValue(value);
+        } catch (IOException e) {
+            throw JacksonUtils.convertException(e);
+        }
+        return this;
+    }
+
+    @Override
+    public JsonGenerator write(String name, String value) {
+        try {
+            generator.writeFieldName(name);
+            generator.writeString(value);
+        } catch (IOException e) {
+            throw JacksonUtils.convertException(e);
+        }
+        return this;
+    }
+
+    @Override
+    public JsonGenerator write(String name, BigInteger value) {
+        try {
+            generator.writeFieldName(name);
+            generator.writeNumber(value);
+        } catch (IOException e) {
+            throw JacksonUtils.convertException(e);
+        }
+        return this;
+    }
+
+    @Override
+    public JsonGenerator write(String name, BigDecimal value) {
+        try {
+            generator.writeFieldName(name);
+            generator.writeNumber(value);
+        } catch (IOException e) {
+            throw JacksonUtils.convertException(e);
+        }
+        return this;
+    }
+
+    @Override
+    public JsonGenerator write(String name, int value) {
+        try {
+            generator.writeFieldName(name);
+            generator.writeNumber(value);
+        } catch (IOException e) {
+            throw JacksonUtils.convertException(e);
+        }
+        return this;
+    }
+
+    @Override
+    public JsonGenerator write(String name, long value) {
+        try {
+            generator.writeFieldName(name);
+            generator.writeNumber(value);
+        } catch (IOException e) {
+            throw JacksonUtils.convertException(e);
+        }
+        return this;
+    }
+
+    @Override
+    public JsonGenerator write(String name, double value) {
+        try {
+            generator.writeFieldName(name);
+            generator.writeNumber(value);
+        } catch (IOException e) {
+            throw JacksonUtils.convertException(e);
+        }
+        return this;
+    }
+
+    @Override
+    public JsonGenerator write(String name, boolean value) {
+        try {
+            generator.writeFieldName(name);
+            generator.writeBooleanField(name, value);
+        } catch (IOException e) {
+            throw JacksonUtils.convertException(e);
+        }
+        return this;
+    }
+
+    @Override
+    public JsonGenerator writeNull(String name) {
+        try {
+            generator.writeFieldName(name);
+            generator.writeNull();
+        } catch (IOException e) {
+            throw JacksonUtils.convertException(e);
+        }
+        return this;
+    }
+
+    @Override
+    public JsonGenerator writeEnd() {
+        try {
+            JsonStreamContext ctx = generator.getOutputContext();
+            if (ctx.inObject()) {
+                generator.writeEndObject();
+            } else if (ctx.inArray()) {
+                generator.writeEndArray();
+            } else {
+                throw new JsonGenerationException("Unexpected context: '" + ctx.typeDesc() + "'");
+            }
+        } catch (IOException e) {
+            throw JacksonUtils.convertException(e);
+        }
+        return this;
+    }
+
+    @Override
+    public JsonGenerator write(JsonValue value) {
+        try {
+            writeValue(value);
+        } catch (IOException e) {
+            throw JacksonUtils.convertException(e);
+        }
+        return this;
+    }
+
+    @Override
+    public JsonGenerator write(String value) {
+        try {
+            generator.writeString(value);
+        } catch (IOException e) {
+            throw JacksonUtils.convertException(e);
+        }
+        return this;
+    }
+
+    @Override
+    public JsonGenerator write(BigDecimal value) {
+        try {
+            generator.writeNumber(value);
+        } catch (IOException e) {
+            throw JacksonUtils.convertException(e);
+        }
+        return this;
+    }
+
+    @Override
+    public JsonGenerator write(BigInteger value) {
+        try {
+            generator.writeNumber(value);
+        } catch (IOException e) {
+            throw JacksonUtils.convertException(e);
+        }
+        return this;
+    }
+
+    @Override
+    public JsonGenerator write(int value) {
+        try {
+            generator.writeNumber(value);
+        } catch (IOException e) {
+            throw JacksonUtils.convertException(e);
+        }
+        return this;
+    }
+
+    @Override
+    public JsonGenerator write(long value) {
+        try {
+            generator.writeNumber(value);
+        } catch (IOException e) {
+            throw JacksonUtils.convertException(e);
+        }
+        return this;
+    }
+
+    @Override
+    public JsonGenerator write(double value) {
+        try {
+            generator.writeNumber(value);
+        } catch (IOException e) {
+            throw JacksonUtils.convertException(e);
+        }
+        return this;
+    }
+
+    @Override
+    public JsonGenerator write(boolean value) {
+        try {
+            generator.writeBoolean(value);
+        } catch (IOException e) {
+            throw JacksonUtils.convertException(e);
+        }
+        return this;
+    }
+
+    @Override
+    public JsonGenerator writeNull() {
+        try {
+            generator.writeNull();
+        } catch (IOException e) {
+            throw JacksonUtils.convertException(e);
+        }
+        return this;
+    }
+
+    @Override
+    public void close() {
+        try {
+            generator.close();
+        } catch (IOException e) {
+            throw JacksonUtils.convertException(e);
+        }
+    }
+
+    @Override
+    public void flush() {
+        try {
+            generator.flush();
+        } catch (IOException e) {
+            throw JacksonUtils.convertException(e);
+        }
+    }
+
+    private void writeValue(JsonValue value) throws IOException {
+        switch (value.getValueType()) {
+            case OBJECT:
+                generator.writeStartObject();
+                for (Map.Entry<String, JsonValue> entry: value.asJsonObject().entrySet()) {
+                    generator.writeFieldName(entry.getKey());
+                    writeValue(entry.getValue());
+                }
+                generator.writeEndObject();
+                break;
+
+            case ARRAY:
+                generator.writeStartArray();
+                for (JsonValue item: value.asJsonArray()) {
+                    writeValue(item);
+                }
+                generator.writeEndArray();
+                break;
+
+            case STRING:
+                generator.writeString(((JsonString)value).getString());
+                break;
+
+            case FALSE:
+                generator.writeBoolean(false);
+                break;
+
+            case TRUE:
+                generator.writeBoolean(true);
+                break;
+
+            case NULL:
+                generator.writeNull();
+                break;
+
+            case NUMBER:
+                JsonNumber n = (JsonNumber) value;
+                if (n.isIntegral()) {
+                    generator.writeNumber(n.longValue());
+                } else {
+                    generator.writeNumber(n.doubleValue());
+                }
+                break;
+        }
+    }
+}
diff --git a/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonJsonpLocation.java b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonJsonpLocation.java
new file mode 100644
index 0000000000..132f40365e
--- /dev/null
+++ b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonJsonpLocation.java
@@ -0,0 +1,54 @@
+/****************************************************************
+ * 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.james.backends.opensearch.json.jackson;
+
+import jakarta.json.stream.JsonLocation;
+
+/**
+ * Translate a Jackson location to a JSONP location.
+ */
+public class JacksonJsonpLocation implements JsonLocation {
+
+    private final com.fasterxml.jackson.core.JsonLocation location;
+
+    JacksonJsonpLocation(com.fasterxml.jackson.core.JsonLocation location) {
+        this.location = location;
+    }
+
+    JacksonJsonpLocation(com.fasterxml.jackson.core.JsonParser parser) {
+        this(parser.getTokenLocation());
+    }
+
+    @Override
+    public long getLineNumber() {
+        return location.getLineNr();
+    }
+
+    @Override
+    public long getColumnNumber() {
+        return location.getColumnNr();
+    }
+
+    @Override
+    public long getStreamOffset() {
+        long charOffset = location.getCharOffset();
+        return charOffset == -1 ? location.getByteOffset() : charOffset;
+    }
+}
diff --git a/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonJsonpMapper.java b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonJsonpMapper.java
new file mode 100644
index 0000000000..692b0e937c
--- /dev/null
+++ b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonJsonpMapper.java
@@ -0,0 +1,123 @@
+/****************************************************************
+ * 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.james.backends.opensearch.json.jackson;
+
+import java.io.IOException;
+import java.util.EnumSet;
+
+import org.apache.james.backends.opensearch.json.JsonpDeserializerBase;
+import org.apache.james.backends.opensearch.json.JsonpMapperBase;
+import org.opensearch.client.json.JsonpDeserializer;
+import org.opensearch.client.json.JsonpMapper;
+import org.opensearch.client.json.JsonpSerializer;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+
+import jakarta.json.spi.JsonProvider;
+import jakarta.json.stream.JsonGenerator;
+import jakarta.json.stream.JsonParser;
+
+public class JacksonJsonpMapper extends JsonpMapperBase {
+
+    private final JacksonJsonProvider provider;
+    private final ObjectMapper objectMapper;
+
+    public JacksonJsonpMapper(ObjectMapper objectMapper) {
+        this(objectMapper, new JsonFactory());
+    }
+
+    public JacksonJsonpMapper(ObjectMapper objectMapper, JsonFactory jsonFactory) {
+        this.provider = new JacksonJsonProvider(jsonFactory);
+        this.objectMapper = objectMapper
+            .configure(SerializationFeature.INDENT_OUTPUT, false)
+            .setSerializationInclusion(JsonInclude.Include.NON_NULL);
+    }
+
+    public JacksonJsonpMapper() {
+        this(new ObjectMapper());
+    }
+
+    /**
+     * Returns the underlying Jackson mapper.
+     */
+    public ObjectMapper objectMapper() {
+        return this.objectMapper;
+    }
+
+    @Override
+    public JsonProvider jsonProvider() {
+        return provider;
+    }
+
+    @Override
+    protected  <T> JsonpDeserializer<T> getDefaultDeserializer(Class<T> clazz) {
+        return new JacksonValueParser<>(clazz);
+    }
+
+    @Override
+    public <T> void serialize(T value, JsonGenerator generator) {
+
+        if (!(generator instanceof JacksonJsonpGenerator)) {
+            throw new IllegalArgumentException("Jackson's ObjectMapper can only be used with the JacksonJsonpProvider");
+        }
+
+        JsonpSerializer<T> serializer = findSerializer(value);
+        if (serializer != null) {
+            serializer.serialize(value, generator, this);
+            return;
+        }
+
+        com.fasterxml.jackson.core.JsonGenerator jkGenerator = ((JacksonJsonpGenerator)generator).jacksonGenerator();
+        try {
+            objectMapper.writeValue(jkGenerator, value);
+        } catch (IOException ioe) {
+            throw JacksonUtils.convertException(ioe);
+        }
+    }
+
+    private class JacksonValueParser<T> extends JsonpDeserializerBase<T> {
+
+        private final Class<T> clazz;
+
+        protected JacksonValueParser(Class<T> clazz) {
+            super(EnumSet.allOf(JsonParser.Event.class));
+            this.clazz = clazz;
+        }
+
+        @Override
+        public T deserialize(JsonParser parser, JsonpMapper mapper, JsonParser.Event event) {
+
+            if (!(parser instanceof JacksonJsonpParser)) {
+                throw new IllegalArgumentException("Jackson's ObjectMapper can only be used with the JacksonJsonpProvider");
+            }
+
+            com.fasterxml.jackson.core.JsonParser jkParser = ((JacksonJsonpParser)parser).jacksonParser();
+
+            try {
+                return objectMapper.readValue(jkParser, clazz);
+            } catch (IOException ioe) {
+                throw JacksonUtils.convertException(ioe);
+            }
+        }
+    }
+}
diff --git a/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonJsonpParser.java b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonJsonpParser.java
new file mode 100644
index 0000000000..b1dcacccb2
--- /dev/null
+++ b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonJsonpParser.java
@@ -0,0 +1,313 @@
+/****************************************************************
+ * 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.james.backends.opensearch.json.jackson;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.EnumMap;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.stream.Stream;
+
+import com.fasterxml.jackson.core.JsonToken;
+
+import jakarta.json.JsonArray;
+import jakarta.json.JsonObject;
+import jakarta.json.JsonValue;
+import jakarta.json.stream.JsonLocation;
+import jakarta.json.stream.JsonParser;
+import jakarta.json.stream.JsonParsingException;
+
+/**
+ * A JSONP parser implementation on top of Jackson.
+ * <p>
+ * <b>Warning:</b> this implementation isn't fully compliant with the JSONP specification: calling {@link #hasNext()}
+ * moves forward the underlying Jackson parser as Jackson doesn't provide an equivalent method. This means no value
+ * getter method (e.g. {@link #getInt()} or {@link #getString()} should be called until the next call to {@link #next()}.
+ * Such calls will throw an {@code IllegalStateException}.
+ */
+public class JacksonJsonpParser implements JsonParser {
+
+    private final com.fasterxml.jackson.core.JsonParser parser;
+
+    private boolean hasNextWasCalled = false;
+
+    private static final EnumMap<JsonToken, Event> tokenToEvent;
+
+    static {
+        tokenToEvent = new EnumMap<>(JsonToken.class);
+        tokenToEvent.put(JsonToken.END_ARRAY, Event.END_ARRAY);
+        tokenToEvent.put(JsonToken.END_OBJECT, Event.END_OBJECT);
+        tokenToEvent.put(JsonToken.FIELD_NAME, Event.KEY_NAME);
+        tokenToEvent.put(JsonToken.START_ARRAY, Event.START_ARRAY);
+        tokenToEvent.put(JsonToken.START_OBJECT, Event.START_OBJECT);
+        tokenToEvent.put(JsonToken.VALUE_FALSE, Event.VALUE_FALSE);
+        tokenToEvent.put(JsonToken.VALUE_NULL, Event.VALUE_NULL);
+        tokenToEvent.put(JsonToken.VALUE_NUMBER_FLOAT, Event.VALUE_NUMBER);
+        tokenToEvent.put(JsonToken.VALUE_NUMBER_INT, Event.VALUE_NUMBER);
+        tokenToEvent.put(JsonToken.VALUE_STRING, Event.VALUE_STRING);
+        tokenToEvent.put(JsonToken.VALUE_TRUE, Event.VALUE_TRUE);
+
+        // No equivalent for
+        // - VALUE_EMBEDDED_OBJECT
+        // - NOT_AVAILABLE
+    }
+
+    public JacksonJsonpParser(com.fasterxml.jackson.core.JsonParser parser) {
+        this.parser = parser;
+    }
+
+    /**
+     * Returns the underlying Jackson parser.
+     */
+    public com.fasterxml.jackson.core.JsonParser jacksonParser() {
+        return this.parser;
+    }
+
+    private JsonParsingException convertException(IOException ioe) {
+        return new JsonParsingException("Jackson exception: " + ioe.getMessage(), ioe, getLocation());
+    }
+
+    private JsonToken fetchNextToken() {
+        try {
+            return parser.nextToken();
+        } catch (IOException e) {
+            throw convertException(e);
+        }
+    }
+
+    private void ensureTokenIsCurrent() {
+        if (hasNextWasCalled) {
+            throw new IllegalStateException("Cannot get event data as parser as already been moved to the next event");
+        }
+    }
+
+    @Override
+    public boolean hasNext() {
+        if (hasNextWasCalled) {
+            return parser.currentToken() != null;
+        } else {
+            hasNextWasCalled = true;
+            return fetchNextToken() != null;
+        }
+    }
+
+    @Override
+    public Event next() {
+        JsonToken token;
+        if (hasNextWasCalled) {
+            token = parser.getCurrentToken();
+            hasNextWasCalled = false;
+        } else {
+            token = fetchNextToken();
+        }
+
+        if (token == null) {
+            throw new NoSuchElementException();
+        }
+
+        Event result = tokenToEvent.get(token);
+        if (result == null) {
+            throw new JsonParsingException("Unsupported Jackson event type '" + token + "'", getLocation());
+        }
+
+        return result;
+    }
+
+    @Override
+    public String getString() {
+        ensureTokenIsCurrent();
+        try {
+            return parser.getValueAsString();
+        } catch (IOException e) {
+            throw convertException(e);
+        }
+    }
+
+    @Override
+    public boolean isIntegralNumber() {
+        ensureTokenIsCurrent();
+        return parser.isExpectedNumberIntToken();
+    }
+
+    @Override
+    public int getInt() {
+        ensureTokenIsCurrent();
+        try {
+            return parser.getIntValue();
+        } catch (IOException e) {
+            throw convertException(e);
+        }
+    }
+
+    @Override
+    public long getLong() {
+        ensureTokenIsCurrent();
+        try {
+            return parser.getLongValue();
+        } catch (IOException e) {
+            throw convertException(e);
+        }
+    }
+
+    @Override
+    public BigDecimal getBigDecimal() {
+        ensureTokenIsCurrent();
+        try {
+            return parser.getDecimalValue();
+        } catch (IOException e) {
+            throw convertException(e);
+        }
+    }
+
+    @Override
+    public JsonLocation getLocation() {
+        return new JacksonJsonpLocation(parser.getCurrentLocation());
+    }
+
+    @Override
+    public void close() {
+        try {
+            parser.close();
+        } catch (IOException e) {
+            throw convertException(e);
+        }
+    }
+
+    private JsonValueParser valueParser;
+
+    @Override
+    public JsonObject getObject() {
+        ensureTokenIsCurrent();
+        if (parser.currentToken() != JsonToken.START_OBJECT) {
+            throw new IllegalStateException("Unexpected event '" + parser.currentToken() +
+                "' at " + parser.getTokenLocation());
+        }
+        if (valueParser == null) {
+            valueParser = new JsonValueParser();
+        }
+        try {
+            return valueParser.parseObject(parser);
+        } catch (IOException e) {
+            throw convertException(e);
+        }
+    }
+
+    @Override
+    public JsonArray getArray() {
+        ensureTokenIsCurrent();
+        if (valueParser == null) {
+            valueParser = new JsonValueParser();
+        }
+        if (parser.currentToken() != JsonToken.START_ARRAY) {
+            throw new IllegalStateException("Unexpected event '" + parser.currentToken() +
+                "' at " + parser.getTokenLocation());
+        }
+        try {
+            return valueParser.parseArray(parser);
+        } catch (IOException e) {
+            throw convertException(e);
+        }
+    }
+
+    @Override
+    public JsonValue getValue() {
+        ensureTokenIsCurrent();
+        if (valueParser == null) {
+            valueParser = new JsonValueParser();
+        }
+        try {
+            return valueParser.parseValue(parser);
+        } catch (IOException e) {
+            throw convertException(e);
+        }
+    }
+
+    @Override
+    public void skipObject() {
+        ensureTokenIsCurrent();
+        if (parser.currentToken() != JsonToken.START_OBJECT) {
+            return;
+        }
+
+        try {
+            int depth = 1;
+            JsonToken token;
+            do {
+                token = parser.nextToken();
+                switch (token) {
+                    case START_OBJECT:
+                        depth++;
+                        break;
+                    case END_OBJECT:
+                        depth--;
+                        break;
+                }
+            } while (!(token == JsonToken.END_OBJECT && depth == 0));
+        } catch (IOException e) {
+            throw convertException(e);
+        }
+    }
+
+    @Override
+    public void skipArray() {
+        ensureTokenIsCurrent();
+        if (parser.currentToken() != JsonToken.START_ARRAY) {
+            return;
+        }
+
+        try {
+            int depth = 1;
+            JsonToken token;
+            do {
+                token = parser.nextToken();
+                switch (token) {
+                    case START_ARRAY:
+                        depth++;
+                        break;
+                    case END_ARRAY:
+                        depth--;
+                        break;
+                }
+            } while (!(token == JsonToken.END_ARRAY && depth == 0));
+        } catch (IOException e) {
+            throw convertException(e);
+        }
+    }
+
+    @Override
+    public Stream<Map.Entry<String, JsonValue>> getObjectStream() {
+        return getObject().entrySet().stream();
+    }
+
+    @Override
+    public Stream<JsonValue> getArrayStream() {
+        return getArray().stream();
+    }
+
+    /**
+     * Not implemented.
+     */
+    @Override
+    public Stream<JsonValue> getValueStream() {
+        return JsonParser.super.getValueStream();
+    }
+}
+
diff --git a/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonUtils.java b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonUtils.java
new file mode 100644
index 0000000000..d5e7782395
--- /dev/null
+++ b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonUtils.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.james.backends.opensearch.json.jackson;
+
+import java.io.IOException;
+
+import com.fasterxml.jackson.core.JsonParseException;
+
+import jakarta.json.JsonException;
+import jakarta.json.stream.JsonGenerationException;
+import jakarta.json.stream.JsonParsingException;
+
+class JacksonUtils {
+    public static JsonException convertException(IOException ioe) {
+        if (ioe instanceof com.fasterxml.jackson.core.JsonGenerationException) {
+            return new JsonGenerationException(ioe.getMessage(), ioe);
+
+        } else if (ioe instanceof JsonParseException) {
+            JsonParseException jpe = (JsonParseException) ioe;
+            return new JsonParsingException(ioe.getMessage(), jpe, new JacksonJsonpLocation(jpe.getLocation()));
+
+        } else {
+            return new JsonException("Jackson exception", ioe);
+        }
+    }
+}
diff --git a/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JsonValueParser.java b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JsonValueParser.java
new file mode 100644
index 0000000000..a66bbd3edb
--- /dev/null
+++ b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JsonValueParser.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.james.backends.opensearch.json.jackson;
+
+import java.io.IOException;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+
+import jakarta.json.JsonArray;
+import jakarta.json.JsonArrayBuilder;
+import jakarta.json.JsonObject;
+import jakarta.json.JsonObjectBuilder;
+import jakarta.json.JsonValue;
+import jakarta.json.spi.JsonProvider;
+import jakarta.json.stream.JsonParsingException;
+
+/**
+ * Reads a Jsonp value/object/array from a Jackson parser. The parser's current token should be the start of the
+ * object (e.g. START_OBJECT, VALUE_NUMBER, etc).
+ */
+class JsonValueParser {
+    private static final JsonProvider provider = JsonProvider.provider();
+
+    public JsonObject parseObject(JsonParser parser) throws IOException {
+
+        JsonObjectBuilder ob = provider.createObjectBuilder();
+
+        JsonToken token;
+        while ((token = parser.nextToken()) != JsonToken.END_OBJECT) {
+            if (token != JsonToken.FIELD_NAME) {
+                throw new JsonParsingException("Expected a property name", new JacksonJsonpLocation(parser));
+            }
+            String name = parser.getCurrentName();
+            parser.nextToken();
+            ob.add(name, parseValue(parser));
+        }
+        return ob.build();
+    }
+
+    public JsonArray parseArray(JsonParser parser) throws IOException {
+        JsonArrayBuilder ab = provider.createArrayBuilder();
+
+        while (parser.nextToken() != JsonToken.END_ARRAY) {
+            ab.add(parseValue(parser));
+        }
+        return ab.build();
+    }
+
+    public JsonValue parseValue(JsonParser parser) throws IOException {
+        switch (parser.currentToken()) {
+            case START_OBJECT:
+                return parseObject(parser);
+
+            case START_ARRAY:
+                return parseArray(parser);
+
+            case VALUE_TRUE:
+                return JsonValue.TRUE;
+
+            case VALUE_FALSE:
+                return JsonValue.FALSE;
+
+            case VALUE_NULL:
+                return JsonValue.NULL;
+
+            case VALUE_STRING:
+                return provider.createValue(parser.getText());
+
+            case VALUE_NUMBER_FLOAT:
+            case VALUE_NUMBER_INT:
+                switch (parser.getNumberType()) {
+                    case INT:
+                        return provider.createValue(parser.getIntValue());
+                    case LONG:
+                        return provider.createValue(parser.getLongValue());
+                    case FLOAT:
+                    case DOUBLE:
+                        return provider.createValue(parser.getDoubleValue());
+                    case BIG_DECIMAL:
+                        return provider.createValue(parser.getDecimalValue());
+                    case BIG_INTEGER:
+                        return provider.createValue(parser.getBigIntegerValue());
+                }
+
+            default:
+                throw new JsonParsingException("Unexpected token '" + parser.currentToken() + "'", new JacksonJsonpLocation(parser));
+
+        }
+    }
+}
diff --git a/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/package-info.java b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/package-info.java
new file mode 100644
index 0000000000..db5d3f4c13
--- /dev/null
+++ b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/package-info.java
@@ -0,0 +1,29 @@
+/****************************************************************
+ * 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.james.backends.opensearch.json;
+
+/**
+ * See {@link https://github.com/opensearch-project/opensearch-java/issues/292}
+ *
+ * The Jackson related code was copied from {@link https://github.com/opensearch-project/opensearch-java} in order to get rid of a SPI lookup for each deserialized request,
+ * that was happening on the event loop.
+ *
+ * The only modified class is {@link jackson.JsonValueParser} where the SPI lookup was made static.
+ */
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org