Repository: helidon-io/helidon (2026-03-24T14:54:41Z)

Issue:
DbMapper is not invoked for DML operations
## Environment Details
* Helidon Version: 4.3.1
* Helidon SE 
* JDK version: 21
* OS: macOS

----------

## Problem Description
DbMapper is not applied when executing a DML statements
Easily reproducible

## Steps to reproduce
1. Create custom DbMapper and implement abstract methods 
2. Register custom DbMapper with DbMapperProvider
3. Apply provider to the DbClient configuration 
4. Create DbExecute and DbStatementDml
5. Set relevant type of parameter and execute the statement
6. Place breakpoints into DbMapper methods"
7. Run application in debug mode.
8. Observe statement execution exceptions that are the result of the DbMapper not applied.
9. Notice that one of the breakpoints in the DbMaper where ever triggered.
10. Try to execute DbStatementGet with the same mapper.
11. Notice the query operation works and the relevant breakpoint is triggered in the DbMapper.

[shopHdbClient.zip](https://github.com/user-attachments/files/23418837/shopHdbClient.zip)






Discussion:
@hyder
Update: After extensive testing I figured out that the the DbMapper is ONLY triggered when application set parameters via these methods calls:
```java
statement.indexedParam(obj);
statement.namedParam(obj);
```
but NOT via any of the addParam or addParams methods variants. 
Also, counterintuitively the statement has to specify the exact structure of what parameters should be mapped to, not what you set as a parameter, i.e. you set an object (in my example instance of Product) as an argument, but the query should layout the exact structure to which his object should be mapped by the DbMapper:
```json
create-product3: '{
  "collection": "products",
  "operation": "insert",
  "value": {
    "id": $id,
    "name": $name,
    "price": $price,
    "bestBefore": $bestBefore,
    "category": $category,
    "version": $version
  }
}'
```
Not this:
```json
create-product1: '{
  "collection": "products",
  "operation": "insert",
  "value": {
    "product": $product
  }
}'
```
In my actual example I'm also using an array of reviews, but I did not yet figure out what the syntax of the statement for this structure should be, but since this array is an optional element according to the JSON Schema it can be omitted to make this test work and demonstrate the conditions under which the DbMapper invocation is triggered.
Tracking PRs for #10819:

- `helidon-4.x`: #11405
- `main` forward port: #11407

Keep this issue open until both PRs are merged.




Patch:
diff --git a/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbStatement.java b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbStatement.java
index 707c5eb815a..f119fc3fdf0 100644
--- a/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbStatement.java
+++ b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbStatement.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, 2023 Oracle and/or its affiliates.
+ * Copyright (c) 2019, 2026 Oracle and/or its affiliates.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -15,13 +15,19 @@
  */
 package io.helidon.dbclient.mongodb;
 
+import java.lang.reflect.Array;
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
+import io.helidon.common.GenericType;
+import io.helidon.common.mapper.MapperException;
 import io.helidon.dbclient.DbClientServiceContext;
 import io.helidon.dbclient.DbExecuteContext;
 import io.helidon.dbclient.DbIndexedStatementParameters;
+import io.helidon.dbclient.DbMapperManager;
 import io.helidon.dbclient.DbNamedStatementParameters;
 import io.helidon.dbclient.DbStatement;
 import io.helidon.dbclient.DbStatementBase;
@@ -45,6 +51,8 @@
  */
 abstract class MongoDbStatement<S extends DbStatement<S>> extends DbStatementBase<S> {
 
+    private static final String ERROR_NO_MAPPER_FOUND = "Failed to find DB mapper.";
+
     /**
      * Empty JSON object.
      */
@@ -106,15 +114,126 @@ String prepareStatement(DbClientServiceContext serviceContext) {
         String statement = serviceContext.statement();
         DbStatementParameters stmtParams = serviceContext.statementParameters();
         if (stmtParams instanceof DbIndexedStatementParameters indexed) {
-            List<Object> params = indexed.parameters();
+            List<Object> params = indexed.parameters().stream()
+                    .map(this::normalizeParameter)
+                    .toList();
             return StatementParsers.indexedParser(statement, params).convert();
         } else if (stmtParams instanceof DbNamedStatementParameters named) {
-            Map<String, Object> params = named.parameters();
-            return StatementParsers.namedParser(statement, params).convert();
+            return StatementParsers.namedParser(statement, prepareNamedParameters(named)).convert();
         }
         return statement;
     }
 
+    private Map<String, Object> prepareNamedParameters(DbNamedStatementParameters named) {
+        Map<String, Object> params = new LinkedHashMap<>(named.parameters().size());
+        Map<String, Object> flattened = new LinkedHashMap<>();
+        named.parameters().forEach((key, value) -> {
+            NamedParameter namedParameter = normalizeNamedParameter(value);
+            params.put(key, namedParameter.value());
+            namedParameter.flattenedValues().forEach(flattened::putIfAbsent);
+        });
+        flattened.forEach(params::putIfAbsent);
+        return params;
+    }
+
+    private Object normalizeParameter(Object value) {
+        if ((value == null) || isScalar(value)) {
+            return value;
+        }
+        if (value instanceof Map<?, ?> valueMap) {
+            return normalizeMap(valueMap);
+        }
+        if (value instanceof Iterable<?> iterable) {
+            return normalizeList(iterable);
+        }
+        if (value.getClass().isArray()) {
+            return normalizeArray(value);
+        }
+        Map<String, Object> mappedValue = mappedValue(value);
+        return mappedValue == null ? value : mappedValue;
+    }
+
+    private NamedParameter normalizeNamedParameter(Object value) {
+        if ((value == null) || isScalar(value)) {
+            return NamedParameter.create(value);
+        }
+        if (value instanceof Map<?, ?> valueMap) {
+            return NamedParameter.create(normalizeMap(valueMap));
+        }
+        if (value instanceof Iterable<?> iterable) {
+            return NamedParameter.create(normalizeList(iterable));
+        }
+        if (value.getClass().isArray()) {
+            return NamedParameter.create(normalizeArray(value));
+        }
+        Map<String, Object> mappedValue = mappedValue(value);
+        if (mappedValue == null) {
+            return NamedParameter.create(value);
+        }
+        return new NamedParameter(mappedValue, mappedValue);
+    }
+
+    private Map<String, Object> normalizeMap(Map<?, ?> valueMap) {
+        Map<String, Object> normalized = new LinkedHashMap<>(valueMap.size());
+        valueMap.forEach((key, nestedValue) -> normalized.put(String.valueOf(key), normalizeParameter(nestedValue)));
+        return normalized;
+    }
+
+    private List<Object> normalizeList(Iterable<?> iterable) {
+        List<Object> normalized = new ArrayList<>();
+        iterable.forEach(nestedValue -> normalized.add(normalizeParameter(nestedValue)));
+        return normalized;
+    }
+
+    private List<Object> normalizeArray(Object value) {
+        int length = Array.getLength(value);
+        List<Object> normalized = new ArrayList<>(length);
+        for (int i = 0; i < length; i++) {
+            normalized.add(normalizeParameter(Array.get(value, i)));
+        }
+        return normalized;
+    }
+
+    @SuppressWarnings("unchecked")
+    private Map<String, Object> mappedValue(Object value) {
+        Class<Object> valueClass = (Class<Object>) value.getClass();
+        try {
+            return normalizeMap(context().dbMapperManager().toNamedParameters(value, valueClass));
+        } catch (MapperException e) {
+            if (isMissingMapper(e, valueClass)) {
+                return null;
+            }
+            throw e;
+        }
+    }
+
+    private boolean isMissingMapper(MapperException exception, Class<?> valueClass) {
+        // DbMapperManager only exposes mapping operations, so we fall back
+        // only when the manager reports its specific "no mapper found" error.
+        String expectedMessage = "Failed to map "
+                + GenericType.create(valueClass).getTypeName()
+                + " to "
+                + DbMapperManager.TYPE_NAMED_PARAMS.getTypeName()
+                + ": "
+                + ERROR_NO_MAPPER_FOUND;
+        return expectedMessage.equals(exception.getMessage());
+    }
+
+    private static boolean isScalar(Object value) {
+        return value instanceof CharSequence
+                || value instanceof Number
+                || value instanceof Boolean
+                || value instanceof Character
+                || value instanceof Enum<?>
+                || value instanceof jakarta.json.JsonValue;
+    }
+
+    private record NamedParameter(Object value, Map<String, Object> flattenedValues) {
+        private static NamedParameter create(Object value) {
+            return new NamedParameter(value, Map.of());
+        }
+    }
+
     /**
      * Mongo operation enumeration.
      */
diff --git a/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/StatementParsers.java b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/StatementParsers.java
index 043c3bef086..eafff1ff376 100644
--- a/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/StatementParsers.java
+++ b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/StatementParsers.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, 2023 Oracle and/or its affiliates.
+ * Copyright (c) 2019, 2026 Oracle and/or its affiliates.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,14 +16,17 @@
 package io.helidon.dbclient.mongodb;
 
 import java.lang.System.Logger.Level;
+import java.lang.reflect.Array;
 import java.math.BigDecimal;
 import java.math.BigInteger;
+import java.util.Iterator;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Map;
 import java.util.function.Consumer;
 
 import jakarta.json.Json;
+import jakarta.json.JsonValue;
 
 /**
  * Statement parameter parsers.
@@ -42,6 +45,21 @@ final class StatementParsers {
      * @return JSON value
      */
     static String toJson(Object value) {
+        if (value == null) {
+            return JsonValue.NULL.toString();
+        }
+        if (value instanceof JsonValue jsonValue) {
+            return jsonValue.toString();
+        }
+        if (value instanceof Map<?, ?> map) {
+            return toJsonObject(map);
+        }
+        if (value instanceof Iterable<?> iterable) {
+            return toJsonArray(iterable);
+        }
+        if (value.getClass().isArray()) {
+            return toJsonArray(value);
+        }
         if ((value instanceof Integer) || (value instanceof Short) || (value instanceof Byte)) {
             return Json.createValue(((Number) value).intValue()).toString();
         }
@@ -68,6 +86,45 @@ static String toJson(Object value) {
         return Json.createValue(String.valueOf(value)).toString();
     }
 
+    private static String toJsonObject(Map<?, ?> value) {
+        StringBuilder builder = new StringBuilder("{");
+        Iterator<? extends Map.Entry<?, ?>> iterator = value.entrySet().iterator();
+        while (iterator.hasNext()) {
+            Map.Entry<?, ?> entry = iterator.next();
+            builder.append(Json.createValue(String.valueOf(entry.getKey())));
+            builder.append(':');
+            builder.append(toJson(entry.getValue()));
+            if (iterator.hasNext()) {
+                builder.append(',');
+            }
+        }
+        return builder.append('}').toString();
+    }
+
+    private static String toJsonArray(Iterable<?> value) {
+        StringBuilder builder = new StringBuilder("[");
+        Iterator<?> iterator = value.iterator();
+        while (iterator.hasNext()) {
+            builder.append(toJson(iterator.next()));
+            if (iterator.hasNext()) {
+                builder.append(',');
+            }
+        }
+        return builder.append(']').toString();
+    }
+
+    private static String toJsonArray(Object value) {
+        StringBuilder builder = new StringBuilder("[");
+        int length = Array.getLength(value);
+        for (int i = 0; i < length; i++) {
+            builder.append(toJson(Array.get(value, i)));
+            if (i + 1 < length) {
+                builder.append(',');
+            }
+        }
+        return builder.append(']').toString();
+    }
+
     private StatementParsers() {
     }
 


Test patch:
diff --git a/dbclient/mongodb/src/test/java/io/helidon/dbclient/mongodb/MongoDbClientTest.java b/dbclient/mongodb/src/test/java/io/helidon/dbclient/mongodb/MongoDbClientTest.java
index 63d849fef24..c100ea7638d 100644
--- a/dbclient/mongodb/src/test/java/io/helidon/dbclient/mongodb/MongoDbClientTest.java
+++ b/dbclient/mongodb/src/test/java/io/helidon/dbclient/mongodb/MongoDbClientTest.java
@@ -16,6 +16,13 @@
 package io.helidon.dbclient.mongodb;
 
 import java.lang.System.Logger.Level;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
 import java.util.function.Consumer;
@@ -23,6 +30,10 @@
 import io.helidon.dbclient.DbClientService;
 import io.helidon.dbclient.DbClientServiceContext;
 import io.helidon.dbclient.DbExecute;
+import io.helidon.dbclient.DbMapper;
+import io.helidon.dbclient.DbMapperManager;
+import io.helidon.dbclient.DbRow;
+import io.helidon.dbclient.spi.DbMapperProvider;
 
 import com.mongodb.client.MongoClient;
 import com.mongodb.client.MongoCollection;
@@ -30,6 +41,7 @@
 import org.bson.Document;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mockito;
 
 import static org.hamcrest.MatcherAssert.assertThat;
@@ -37,6 +49,7 @@
 import static org.hamcrest.Matchers.notNullValue;
 import static org.junit.jupiter.api.Assertions.fail;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 @SuppressWarnings("resource")
@@ -106,28 +119,343 @@ void testDbClientServiceDml() {
         TestDbClientService service = new TestDbClientService();
         MongoDbClient dbClient = createClient(builder -> builder.addService(service));
         DbExecute exec = dbClient.execute();
-        long _ = exec.insert("{"
-                                     + "\"collection\": \"foo\","
-                                     + "\"operation\": \"insert\","
-                                     + "\"value\": { \"name\": \"bar\" }"
-                                     + "}");
+        long _ = exec.insert("""
+                                        {
+                                          "collection": "foo",
+                                          "operation": "insert",
+                                          "value": { "name": "bar" }
+                                        }
+                                        """);
         assertThat(service.resultFuture.isDone(), is(true));
         assertThat(service.resultFuture.isCompletedExceptionally(), is(false));
         assertThat(service.statementFuture.isDone(), is(true));
         assertThat(service.statementFuture.isCompletedExceptionally(), is(false));
     }
 
+    @Test
+    void testMappedNamedAddParamDml() {
+        MongoCollection<Document> collection = Mockito.mock(MongoCollection.class);
+        MongoDbClient dbClient = createClient(collection, builder -> builder.dbMapperManager(dbMapperManager()));
+        long result = dbClient.execute()
+                .createInsert("""
+                                      {
+                                        "collection": "foo",
+                                        "operation": "insert",
+                                        "value": { "pokemon": $pokemon }
+                                      }
+                                      """)
+                .addParam("pokemon", new MappedPokemon(25, "Pikachu"))
+                .execute();
+
+        assertThat(result, is(1L));
+
+        ArgumentCaptor<Document> captor = ArgumentCaptor.forClass(Document.class);
+        verify(collection).insertOne(captor.capture());
+        Document pokemon = captor.getValue().get("pokemon", Document.class);
+        assertThat(pokemon.getInteger("id"), is(25));
+        assertThat(pokemon.getString("name"), is("Pikachu"));
+    }
+
+    @Test
+    void testMappedNamedParamDml() {
+        MongoCollection<Document> collection = Mockito.mock(MongoCollection.class);
+        MongoDbClient dbClient = createClient(collection, builder -> builder.dbMapperManager(dbMapperManager()));
+        long result = dbClient.execute()
+                .createInsert("""
+                                      {
+                                        "collection": "foo",
+                                        "operation": "insert",
+                                        "value": {
+                                          "id": $id,
+                                          "name": $name
+                                        }
+                                      }
+                                      """)
+                .namedParam(new MappedPokemon(7, "Squirtle"))
+                .execute();
+
+        assertThat(result, is(1L));
+        assertInsertedPokemon(collection, 7, "Squirtle");
+    }
+
+    @Test
+    void testMappedIndexedParamDml() {
+        MongoCollection<Document> collection = Mockito.mock(MongoCollection.class);
+        MongoDbClient dbClient = createClient(collection, builder -> builder.dbMapperManager(dbMapperManager()));
+        long result = dbClient.execute()
+                .createInsert("""
+                                      {
+                                        "collection": "foo",
+                                        "operation": "insert",
+                                        "value": {
+                                          "id": ?,
+                                          "name": ?
+                                        }
+                                      }
+                                      """)
+                .indexedParam(new MappedPokemon(133, "Eevee"))
+                .execute();
+
+        assertThat(result, is(1L));
+        assertInsertedPokemon(collection, 133, "Eevee");
+    }
+
+    @Test
+    void testMappedIndexedAddParamDml() {
+        MongoCollection<Document> collection = Mockito.mock(MongoCollection.class);
+        MongoDbClient dbClient = createClient(collection, builder -> builder.dbMapperManager(dbMapperManager()));
+        long result = dbClient.execute()
+                .createInsert("""
+                                      {
+                                        "collection": "foo",
+                                        "operation": "insert",
+                                        "value": ?
+                                      }
+                                      """)
+                .addParam(new MappedPokemon(26, "Raichu"))
+                .execute();
+
+        assertThat(result, is(1L));
+
+        assertInsertedPokemon(collection, 26, "Raichu");
+    }
+
+    @Test
+    void testMappedIndexedParamsListDml() {
+        MongoCollection<Document> collection = Mockito.mock(MongoCollection.class);
+        MongoDbClient dbClient = createClient(collection, builder -> builder.dbMapperManager(dbMapperManager()));
+        long result = dbClient.execute()
+                .createInsert("""
+                                      {
+                                        "collection": "foo",
+                                        "operation": "insert",
+                                        "value": ?
+                                      }
+                                      """)
+                .params(List.of(new MappedPokemon(39, "Jigglypuff")))
+                .execute();
+
+        assertThat(result, is(1L));
+        assertInsertedPokemon(collection, 39, "Jigglypuff");
+    }
+
+    @Test
+    void testMappedIndexedParamsVarargsDml() {
+        MongoCollection<Document> collection = Mockito.mock(MongoCollection.class);
+        MongoDbClient dbClient = createClient(collection, builder -> builder.dbMapperManager(dbMapperManager()));
+        long result = dbClient.execute()
+                .createInsert("""
+                                      {
+                                        "collection": "foo",
+                                        "operation": "insert",
+                                        "value": ?
+                                      }
+                                      """)
+                .params(new MappedPokemon(52, "Meowth"))
+                .execute();
+
+        assertThat(result, is(1L));
+        assertInsertedPokemon(collection, 52, "Meowth");
+    }
+
+    @Test
+    void testMappedCollectionParamsDml() {
+        MongoCollection<Document> collection = Mockito.mock(MongoCollection.class);
+        MongoDbClient dbClient = createClient(collection, builder -> builder.dbMapperManager(dbMapperManager()));
+        long result = dbClient.execute()
+                .createInsert("""
+                                      {
+                                        "collection": "foo",
+                                        "operation": "insert",
+                                        "value": { "team": $team }
+                                      }
+                                      """)
+                .params(Map.of("team", List.of(
+                        new MappedPokemon(1, "Bulbasaur"),
+                        new MappedPokemon(4, "Charmander"))))
+                .execute();
+
+        assertThat(result, is(1L));
+
+        ArgumentCaptor<Document> captor = ArgumentCaptor.forClass(Document.class);
+        verify(collection).insertOne(captor.capture());
+        List<Document> team = captor.getValue().getList("team", Document.class);
+        assertThat(team, notNullValue());
+        assertThat(team.size(), is(2));
+        assertThat(team.get(0).getInteger("id"), is(1));
+        assertThat(team.get(0).getString("name"), is("Bulbasaur"));
+        assertThat(team.get(1).getInteger("id"), is(4));
+        assertThat(team.get(1).getString("name"), is("Charmander"));
+    }
+
+    @Test
+    void testIssue10819NamedParamDml() {
+        MongoCollection<Document> collection = Mockito.mock(MongoCollection.class);
+        MongoDbClient dbClient = createClient(collection,
+                                              builder -> builder.dbMapperManager(issue10819DbMapperManager()));
+        Issue10819Product product = issue10819Product();
+        long result = dbClient.execute()
+                .createInsert("""
+                                      {
+                                        "collection": "products",
+                                        "operation": "insert",
+                                        "value": {
+                                          "id": $id,
+                                          "name": $name,
+                                          "price": $price,
+                                          "bestBefore": $bestBefore,
+                                          "category": $category,
+                                          "version": $version,
+                                          "reviews": $reviews
+                                        }
+                                      }
+                                      """)
+                .namedParam(product)
+                .execute();
+
+        assertThat(result, is(1L));
+        assertInsertedIssue10819Product(collection, product);
+    }
+
+    @Test
+    void testIssue10819NamedParamsMapDml() {
+        MongoCollection<Document> collection = Mockito.mock(MongoCollection.class);
+        MongoDbClient dbClient = createClient(collection,
+                                              builder -> builder.dbMapperManager(issue10819DbMapperManager()));
+        Issue10819Product product = issue10819Product();
+        long result = dbClient.execute()
+                .createInsert("""
+                                      {
+                                        "collection": "products",
+                                        "operation": "insert",
+                                        "value": {
+                                          "id": $id,
+                                          "name": $name,
+                                          "price": $price,
+                                          "bestBefore": $bestBefore,
+                                          "category": $category,
+                                          "version": $version,
+                                          "reviews": $reviews
+                                        }
+                                      }
+                                      """)
+                .params(Map.of("product", product))
+                .execute();
+
+        assertThat(result, is(1L));
+        assertInsertedIssue10819Product(collection, product);
+    }
+
+    @Test
+    void testIssue10819IndexedParamDml() {
+        MongoCollection<Document> collection = Mockito.mock(MongoCollection.class);
+        MongoDbClient dbClient = createClient(collection,
+                                              builder -> builder.dbMapperManager(issue10819DbMapperManager()));
+        Issue10819Product product = issue10819Product();
+        long result = dbClient.execute()
+                .createInsert("""
+                                      {
+                                        "collection": "products",
+                                        "operation": "insert",
+                                        "value": {
+                                          "id": ?,
+                                          "name": ?,
+                                          "price": ?,
+                                          "bestBefore": ?,
+                                          "category": ?,
+                                          "version": ?,
+                                          "reviews": ?
+                                        }
+                                      }
+                                      """)
+                .indexedParam(product)
+                .execute();
+
+        assertThat(result, is(1L));
+        assertInsertedIssue10819Product(collection, product);
+    }
+
+    @Test
+    void testIssue10819AttachedReproducer() {
+        MongoCollection<Document> collection = Mockito.mock(MongoCollection.class);
+        MongoDbClient dbClient = createClient(collection,
+                                              builder -> builder.dbMapperManager(issue10819DbMapperManager()));
+        Issue10819Product product = issue10819Product();
+        long result = dbClient.execute()
+                .createInsert("""
+                                      {
+                                        "collection": "products",
+                                        "operation": "insert",
+                                        "value": {
+                                          "id": $id,
+                                          "name": $name,
+                                          "price": $price,
+                                          "bestBefore": $bestBefore,
+                                          "category": $category,
+                                          "version": $version,
+                                          "reviews": $reviews
+                                        }
+                                      }
+                                      """)
+                .addParam("product", product)
+                .execute();
+
+        assertThat(result, is(1L));
+
+        assertInsertedIssue10819Product(collection, product);
+    }
+
+    private static void assertInsertedPokemon(MongoCollection<Document> collection,
+                                              int expectedId,
+                                              String expectedName) {
+        ArgumentCaptor<Document> captor = ArgumentCaptor.forClass(Document.class);
+        verify(collection).insertOne(captor.capture());
+        assertThat(captor.getValue().getInteger("id"), is(expectedId));
+        assertThat(captor.getValue().getString("name"), is(expectedName));
+    }
+
+    private static void assertInsertedIssue10819Product(MongoCollection<Document> collection,
+                                                        Issue10819Product product) {
+        ArgumentCaptor<Document> captor = ArgumentCaptor.forClass(Document.class);
+        verify(collection).insertOne(captor.capture());
+        Document inserted = captor.getValue();
+        assertThat(inserted.getInteger("id"), is(product.getId()));
+        assertThat(inserted.getString("name"), is(product.getName()));
+        assertThat(inserted.getDouble("price"), is(product.getPrice().doubleValue()));
+        assertThat(inserted.getString("bestBefore"), is(product.getBestBefore().toString()));
+        assertThat(inserted.getString("category"), is(product.getCategory()));
+        assertThat(inserted.getInteger("version"), is(product.getVersion()));
+        assertThat(inserted.getList("reviews", Object.class), is(product.getReviews()));
+    }
+
+    private static Issue10819Product issue10819Product() {
+        return new Issue10819Product(110,
+                                     "Cake",
+                                     BigDecimal.valueOf(2.99),
+                                     LocalDate.parse("2025-11-05"),
+                                     "Food",
+                                     1);
+    }
+
     @SuppressWarnings("unchecked")
     static MongoDbClient createClient(Consumer<MongoDbClientBuilder> consumer) {
+        return createClient(null, consumer);
+    }
+
+    @SuppressWarnings("unchecked")
+    static MongoDbClient createClient(MongoCollection<Document> collection, Consumer<MongoDbClientBuilder> consumer) {
         MongoClient client = Mockito.mock(MongoClient.class);
         MongoDatabase db = Mockito.mock(MongoDatabase.class);
-        MongoCollection<Document> collection = Mockito.mock(MongoCollection.class);
-        when(db.getCollection(any())).thenReturn(collection);
+        MongoCollection<Document> mongoCollection = collection == null ? Mockito.mock(MongoCollection.class) : collection;
+        when(db.getCollection(any())).thenReturn(mongoCollection);
         when(db.runCommand(any())).thenReturn(MongoDbStatement.EMPTY);
         MongoDbClientBuilder builder = new MongoDbClientBuilder();
         if (consumer != null) {
             consumer.accept(builder);
         }
+        if (builder.dbMapperManager() == null) {
+            builder.dbMapperManager(DbMapperManager.builder().build());
+        }
         return new MongoDbClient(builder, client, db);
     }
 
@@ -155,4 +483,141 @@ static <T> void setup(CompletionStage<T> stage, CompletableFuture<T> future) {
             });
         }
     }
+
+    record MappedPokemon(int id, String name) {
+    }
+
+    private static final class Issue10819Product {
+        private final int id;
+        private final String name;
+        private final BigDecimal price;
+        private final LocalDate bestBefore;
+        private final String category;
+        private final int version;
+        private final List<Issue10819Review> reviews = new ArrayList<>();
+
+        private Issue10819Product(int id, String name, BigDecimal price, LocalDate bestBefore, String category, int version) {
+            this.id = id;
+            this.name = name;
+            this.price = price;
+            this.bestBefore = bestBefore;
+            this.category = category;
+            this.version = version;
+        }
+
+        int getId() {
+            return id;
+        }
+
+        String getName() {
+            return name;
+        }
+
+        BigDecimal getPrice() {
+            return price;
+        }
+
+        LocalDate getBestBefore() {
+            return bestBefore;
+        }
+
+        String getCategory() {
+            return category;
+        }
+
+        int getVersion() {
+            return version;
+        }
+
+        List<Issue10819Review> getReviews() {
+            return reviews;
+        }
+    }
+
+    private record Issue10819Review(String review, int rating) {
+    }
+
+    private static final class PokemonMapperProvider implements DbMapperProvider {
+        private static final DbMapper<MappedPokemon> POKEMON_MAPPER = new DbMapper<>() {
+            @Override
+            public MappedPokemon read(DbRow row) {
+                throw new UnsupportedOperationException("Read operation is not implemented.");
+            }
+
+            @Override
+            public Map<String, ?> toNamedParameters(MappedPokemon value) {
+                return Map.of(
+                        "id", value.id(),
+                        "name", value.name());
+            }
+
+            @Override
+            public List<?> toIndexedParameters(MappedPokemon value) {
+                return List.of(value.id(), value.name());
+            }
+        };
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T> Optional<DbMapper<T>> mapper(Class<T> type) {
+            if (type.equals(MappedPokemon.class)) {
+                return Optional.of((DbMapper<T>) POKEMON_MAPPER);
+            }
+            return Optional.empty();
+        }
+    }
+
+    private static final class Issue10819ProductMapper implements DbMapper<Issue10819Product> {
+        @Override
+        public Issue10819Product read(DbRow row) {
+            throw new UnsupportedOperationException("Read operation is not implemented.");
+        }
+
+        @Override
+        public Map<String, ?> toNamedParameters(Issue10819Product product) {
+            Map<String, Object> map = new LinkedHashMap<>();
+            map.put("id", product.getId());
+            map.put("name", product.getName());
+            map.put("price", product.getPrice().doubleValue());
+            if (product.getBestBefore() != null) {
+                map.put("bestBefore", product.getBestBefore().toString());
+            }
+            map.put("category", product.getCategory());
+            map.put("version", product.getVersion());
+            if (product.getReviews() != null) {
+                map.put("reviews", product.getReviews());
+            }
+            return map;
+        }
+
+        @Override
+        public List<?> toIndexedParameters(Issue10819Product product) {
+            return List.copyOf(toNamedParameters(product).values());
+        }
+    }
+
+    private static final class Issue10819ProductMapperProvider implements DbMapperProvider {
+        private static final DbMapper<Issue10819Product> PRODUCT_MAPPER = new Issue10819ProductMapper();
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T> Optional<DbMapper<T>> mapper(Class<T> type) {
+            if (type.equals(Issue10819Product.class)) {
+                return Optional.of((DbMapper<T>) PRODUCT_MAPPER);
+            }
+            return Optional.empty();
+        }
+    }
+
+    private static DbMapperManager dbMapperManager() {
+        return DbMapperManager.builder()
+                .addMapperProvider(new PokemonMapperProvider())
+                .build();
+    }
+
+    private static DbMapperManager issue10819DbMapperManager() {
+        return DbMapperManager.builder()
+                .addMapperProvider(new Issue10819ProductMapperProvider())
+                .build();
+    }
 }