Repository: jooby-project/jooby
Pull request #2947 (2023-06-05 00:57:22)

build(deps): bump ebean.version from 13.17.4-jakarta to 13.18.0-jakarta
Bumps `ebean.version` from 13.17.4-jakarta to 13.18.0-jakarta.
Updates `ebean` from 13.17.4-jakarta to 13.18.0-jakarta

Updates `ebean-querybean` from 13.17.4-jakarta to 13.18.0-jakarta

Updates `querybean-generator` from 13.17.4-jakarta to 13.18.0-jakarta

Updates `ebean-test` from 13.17.4-jakarta to 13.18.0-jakarta


Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/jooby/src/main/java/io/jooby/BeanConverter.java b/jooby/src/main/java/io/jooby/BeanConverter.java
index 1e0288ad0..62105e2d0 100644
--- a/jooby/src/main/java/io/jooby/BeanConverter.java
+++ b/jooby/src/main/java/io/jooby/BeanConverter.java
@@ -5,7 +5,10 @@
  */
 package io.jooby;
 
+import java.util.List;
+
 import edu.umd.cs.findbugs.annotations.NonNull;
+import io.jooby.internal.converter.ReflectiveBeanConverter;
 
 /**
  * Value converter for complex values that come from query, path, form, etc... parameters into more
@@ -14,7 +17,6 @@ import edu.umd.cs.findbugs.annotations.NonNull;
  * <p>It is an extension point for {@link ValueNode#to(Class)} calls.
  */
 public interface BeanConverter {
-
   /**
    * True if the converter applies for the given type.
    *
@@ -31,4 +33,17 @@ public interface BeanConverter {
    * @return Converted value.
    */
   Object convert(@NonNull ValueNode node, @NonNull Class type);
+
+  /**
+   * Creates a bean converter that uses reflection.
+   *
+   * @return Reflection bean converter.
+   */
+  static BeanConverter reflective() {
+    return new ReflectiveBeanConverter();
+  }
+
+  static void addFallbackConverters(List<BeanConverter> input) {
+    input.add(reflective());
+  }
 }
diff --git a/jooby/src/main/java/io/jooby/Body.java b/jooby/src/main/java/io/jooby/Body.java
index ffc0fb0f5..c1ed72ae7 100644
--- a/jooby/src/main/java/io/jooby/Body.java
+++ b/jooby/src/main/java/io/jooby/Body.java
@@ -15,7 +15,6 @@ import java.util.List;
 import java.util.Set;
 
 import edu.umd.cs.findbugs.annotations.NonNull;
-import edu.umd.cs.findbugs.annotations.Nullable;
 import io.jooby.exception.MissingValueException;
 import io.jooby.internal.ByteArrayBody;
 import io.jooby.internal.FileBody;
@@ -100,10 +99,6 @@ public interface Body extends ValueNode {
     return to((Type) type);
   }
 
-  default @Nullable @Override <T> T toNullable(@NonNull Class<T> type) {
-    return toNullable((Type) type);
-  }
-
   /**
    * Convert this body into the given type.
    *
@@ -113,15 +108,6 @@ public interface Body extends ValueNode {
    */
   @NonNull <T> T to(@NonNull Type type);
 
-  /**
-   * Convert this body into the given type.
-   *
-   * @param type Type to use.
-   * @param <T> Generic type.
-   * @return Converted value or <code>null</code>.
-   */
-  @Nullable <T> T toNullable(@NonNull Type type);
-
   /* **********************************************************************************************
    * Factory methods:
    * **********************************************************************************************
diff --git a/jooby/src/main/java/io/jooby/Context.java b/jooby/src/main/java/io/jooby/Context.java
index 3d238e73b..a25eb6caf 100644
--- a/jooby/src/main/java/io/jooby/Context.java
+++ b/jooby/src/main/java/io/jooby/Context.java
@@ -30,7 +30,6 @@ import java.util.function.BiFunction;
 
 import edu.umd.cs.findbugs.annotations.NonNull;
 import edu.umd.cs.findbugs.annotations.Nullable;
-import io.jooby.exception.TypeMismatchException;
 import io.jooby.internal.LocaleUtils;
 import io.jooby.internal.ParamLookupImpl;
 import io.jooby.internal.ReadOnlyContext;
@@ -119,23 +118,7 @@ public interface Context extends Registry {
    * @param <T> Generic type.
    * @return Converted value.
    */
-  default @NonNull <T> T convert(@NonNull ValueNode value, @NonNull Class<T> type) {
-    T result = convertOrNull(value, type);
-    if (result == null) {
-      throw new TypeMismatchException(value.name(), type);
-    }
-    return result;
-  }
-
-  /**
-   * Converts a value (single or hash) into the given type.
-   *
-   * @param value Value to convert.
-   * @param type Expected type.
-   * @param <T> Generic type.
-   * @return Converted value or <code>null</code>.
-   */
-  @Nullable <T> T convertOrNull(@NonNull ValueNode value, @NonNull Class<T> type);
+  @NonNull <T> T convert(@NonNull ValueNode value, @NonNull Class<T> type);
 
   /*
    * **********************************************************************************************
diff --git a/jooby/src/main/java/io/jooby/DefaultContext.java b/jooby/src/main/java/io/jooby/DefaultContext.java
index fbd071afd..f9fd561b8 100644
--- a/jooby/src/main/java/io/jooby/DefaultContext.java
+++ b/jooby/src/main/java/io/jooby/DefaultContext.java
@@ -32,6 +32,7 @@ import org.slf4j.Logger;
 import edu.umd.cs.findbugs.annotations.NonNull;
 import edu.umd.cs.findbugs.annotations.Nullable;
 import io.jooby.exception.RegistryException;
+import io.jooby.exception.TypeMismatchException;
 import io.jooby.internal.HashValue;
 import io.jooby.internal.MissingValue;
 import io.jooby.internal.SingleValue;
@@ -405,8 +406,12 @@ public interface DefaultContext extends Context {
   }
 
   @Override
-  default @NonNull <T> T convertOrNull(@NonNull ValueNode value, @NonNull Class<T> type) {
-    return ValueConverters.convert(value, type, getRouter());
+  default @NonNull <T> T convert(@NonNull ValueNode value, @NonNull Class<T> type) {
+    T result = ValueConverters.convert(value, type, getRouter());
+    if (result == null) {
+      throw new TypeMismatchException(value.name(), type);
+    }
+    return result;
   }
 
   @Override
diff --git a/jooby/src/main/java/io/jooby/DefaultErrorHandler.java b/jooby/src/main/java/io/jooby/DefaultErrorHandler.java
index e5edbe781..066db7f91 100644
--- a/jooby/src/main/java/io/jooby/DefaultErrorHandler.java
+++ b/jooby/src/main/java/io/jooby/DefaultErrorHandler.java
@@ -65,7 +65,6 @@ public class DefaultErrorHandler implements ErrorHandler {
 
   @NonNull @Override
   public void apply(@NonNull Context ctx, @NonNull Throwable cause, @NonNull StatusCode code) {
-    log(ctx, cause, code);
     MediaType type = ctx.accept(Arrays.asList(html, json, text));
     if (json.equals(type)) {
       String message = Optional.ofNullable(cause.getMessage()).orElse(code.reason());
diff --git a/jooby/src/main/java/io/jooby/ForwardingContext.java b/jooby/src/main/java/io/jooby/ForwardingContext.java
index 61845cc00..762230c64 100644
--- a/jooby/src/main/java/io/jooby/ForwardingContext.java
+++ b/jooby/src/main/java/io/jooby/ForwardingContext.java
@@ -386,11 +386,6 @@ public class ForwardingContext implements Context {
     return ctx.convert(value, type);
   }
 
-  @Nullable @Override
-  public <T> T convertOrNull(@NonNull ValueNode value, @NonNull Class<T> type) {
-    return ctx.convertOrNull(value, type);
-  }
-
   @NonNull @Override
   public <T> T decode(@NonNull Type type, @NonNull MediaType contentType) {
     return ctx.decode(type, contentType);
diff --git a/jooby/src/main/java/io/jooby/Value.java b/jooby/src/main/java/io/jooby/Value.java
index 8eff0c28b..bfa6f0b63 100644
--- a/jooby/src/main/java/io/jooby/Value.java
+++ b/jooby/src/main/java/io/jooby/Value.java
@@ -405,16 +405,6 @@ public interface Value {
    */
   @NonNull <T> T to(@NonNull Class<T> type);
 
-  /**
-   * Convert this value to the given type. Support values are single-value, array-value and
-   * object-value. Object-value can be converted to a JavaBean type.
-   *
-   * @param type Type to convert.
-   * @param <T> Element type.
-   * @return Instance of the type or <code>null</code>.
-   */
-  @Nullable <T> T toNullable(@NonNull Class<T> type);
-
   /**
    * Value as multi-value map.
    *
diff --git a/jooby/src/main/java/io/jooby/ValueConverter.java b/jooby/src/main/java/io/jooby/ValueConverter.java
index 3139cfc63..4d0d367ad 100644
--- a/jooby/src/main/java/io/jooby/ValueConverter.java
+++ b/jooby/src/main/java/io/jooby/ValueConverter.java
@@ -9,7 +9,6 @@ import java.util.List;
 
 import edu.umd.cs.findbugs.annotations.NonNull;
 import io.jooby.internal.ValueConverters;
-import io.jooby.internal.converter.StringConstructorConverter;
 import io.jooby.internal.converter.ValueOfConverter;
 
 /**
@@ -42,6 +41,5 @@ public interface ValueConverter {
 
   static void addFallbackConverters(List<ValueConverter> input) {
     input.add(new ValueOfConverter());
-    input.add(new StringConstructorConverter());
   }
 }
diff --git a/jooby/src/main/java/io/jooby/internal/ArrayValue.java b/jooby/src/main/java/io/jooby/internal/ArrayValue.java
index 6925afc9e..ac2b289ca 100644
--- a/jooby/src/main/java/io/jooby/internal/ArrayValue.java
+++ b/jooby/src/main/java/io/jooby/internal/ArrayValue.java
@@ -16,7 +16,6 @@ import java.util.Optional;
 import java.util.Set;
 
 import edu.umd.cs.findbugs.annotations.NonNull;
-import edu.umd.cs.findbugs.annotations.Nullable;
 import io.jooby.Context;
 import io.jooby.ValueNode;
 import io.jooby.exception.MissingValueException;
@@ -95,11 +94,6 @@ public class ArrayValue implements ValueNode {
     return ctx.convert(list.get(0), type);
   }
 
-  @Nullable @Override
-  public <T> T toNullable(@NonNull Class<T> type) {
-    return list.isEmpty() ? null : ctx.convertOrNull(list.get(0), type);
-  }
-
   @NonNull @Override
   public <T> List<T> toList(@NonNull Class<T> type) {
     return collect(new ArrayList<>(this.list.size()), type);
diff --git a/jooby/src/main/java/io/jooby/internal/ByteArrayBody.java b/jooby/src/main/java/io/jooby/internal/ByteArrayBody.java
index 13d6a9e3a..21beec56c 100644
--- a/jooby/src/main/java/io/jooby/internal/ByteArrayBody.java
+++ b/jooby/src/main/java/io/jooby/internal/ByteArrayBody.java
@@ -16,7 +16,6 @@ import java.util.List;
 import java.util.Map;
 
 import edu.umd.cs.findbugs.annotations.NonNull;
-import edu.umd.cs.findbugs.annotations.Nullable;
 import io.jooby.Body;
 import io.jooby.Context;
 import io.jooby.MediaType;
@@ -89,11 +88,6 @@ public class ByteArrayBody implements Body {
     return ctx.decode(type, ctx.getRequestType(MediaType.text));
   }
 
-  @Nullable @Override
-  public <T> T toNullable(@NonNull Type type) {
-    return bytes.length == 0 ? null : ctx.decode(type, ctx.getRequestType(MediaType.text));
-  }
-
   @Override
   public Map<String, List<String>> toMultimap() {
     return Collections.emptyMap();
diff --git a/jooby/src/main/java/io/jooby/internal/FileBody.java b/jooby/src/main/java/io/jooby/internal/FileBody.java
index 5f1e2c4af..f374c59ec 100644
--- a/jooby/src/main/java/io/jooby/internal/FileBody.java
+++ b/jooby/src/main/java/io/jooby/internal/FileBody.java
@@ -17,7 +17,6 @@ import java.util.List;
 import java.util.Map;
 
 import edu.umd.cs.findbugs.annotations.NonNull;
-import edu.umd.cs.findbugs.annotations.Nullable;
 import io.jooby.Body;
 import io.jooby.Context;
 import io.jooby.MediaType;
@@ -104,11 +103,6 @@ public class FileBody implements Body {
     return ctx.decode(type, ctx.getRequestType(MediaType.text));
   }
 
-  @Nullable @Override
-  public <T> T toNullable(@NonNull Type type) {
-    return ctx.decode(type, ctx.getRequestType(MediaType.text));
-  }
-
   @Override
   public Map<String, List<String>> toMultimap() {
     return Collections.emptyMap();
diff --git a/jooby/src/main/java/io/jooby/internal/HashValue.java b/jooby/src/main/java/io/jooby/internal/HashValue.java
index 47c977199..56f8cc036 100644
--- a/jooby/src/main/java/io/jooby/internal/HashValue.java
+++ b/jooby/src/main/java/io/jooby/internal/HashValue.java
@@ -5,8 +5,6 @@
  */
 package io.jooby.internal;
 
-import static java.util.Optional.ofNullable;
-
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -23,17 +21,18 @@ import java.util.function.BiConsumer;
 import java.util.function.Supplier;
 
 import edu.umd.cs.findbugs.annotations.NonNull;
-import edu.umd.cs.findbugs.annotations.Nullable;
 import io.jooby.Context;
 import io.jooby.FileUpload;
 import io.jooby.ValueNode;
 
 public class HashValue implements ValueNode {
   private static final Map<String, ValueNode> EMPTY = Collections.emptyMap();
+
   private Context ctx;
+
   private Map<String, ValueNode> hash = EMPTY;
+
   private final String name;
-  private boolean arrayLike;
 
   public HashValue(Context ctx, String name, Supplier<Map<String, ValueNode>> mapSupplier) {
     this.ctx = ctx;
@@ -148,7 +147,6 @@ public class HashValue implements ValueNode {
     if (hash instanceof TreeMap) {
       return;
     }
-    this.arrayLike = true;
     TreeMap<String, ValueNode> ordered = new TreeMap<>();
     ordered.putAll(hash);
     hash.clear();
@@ -242,7 +240,7 @@ public class HashValue implements ValueNode {
     if (hash.isEmpty()) {
       return Optional.empty();
     }
-    return ofNullable(to(type));
+    return Optional.ofNullable(to(type));
   }
 
   @NonNull @Override
@@ -250,15 +248,6 @@ public class HashValue implements ValueNode {
     return ctx.convert(this, type);
   }
 
-  @Nullable @Override
-  public final <T> T toNullable(@NonNull Class<T> type) {
-    return toNullable(ctx, type, allowEmptyBean());
-  }
-
-  protected <T> T toNullable(@NonNull Context ctx, @NonNull Class<T> type, boolean allowEmpty) {
-    return ValueConverters.convert(this, type, ctx.getRouter(), allowEmpty);
-  }
-
   @Override
   public Map<String, List<String>> toMultimap() {
     Map<String, List<String>> result = new LinkedHashMap<>(hash.size());
@@ -288,35 +277,15 @@ public class HashValue implements ValueNode {
   }
 
   private <T, C extends Collection<T>> C toCollection(@NonNull Class<T> type, C collection) {
-    if (!hash.isEmpty()) {
-      if (arrayLike) {
-        // indexes access, treat like a list
-        for (Map.Entry<String, ValueNode> e : hash.entrySet()) {
-          if (e.getKey().chars().allMatch(Character::isDigit)) {
-            // put only [index] where index is a number
-            if (e.getValue() instanceof HashValue node) {
-              addItem(ctx, node, type, collection);
-            } else {
-              ofNullable(e.getValue().toNullable(type)).ifPresent(collection::add);
-            }
-          }
-        }
-      } else {
-        addItem(ctx, this, type, collection);
+    if (hash instanceof TreeMap) {
+      // indexes access, treat like a list
+      Collection<ValueNode> values = hash.values();
+      for (ValueNode value : values) {
+        collection.add(value.to(type));
       }
+    } else {
+      collection.add(to(type));
     }
     return collection;
   }
-
-  private static <T> void addItem(
-      Context ctx, HashValue node, Class<T> type, Collection<T> container) {
-    var item = node.toNullable(ctx, type, false);
-    if (item != null) {
-      container.add(item);
-    }
-  }
-
-  protected boolean allowEmptyBean() {
-    return false;
-  }
 }
diff --git a/jooby/src/main/java/io/jooby/internal/InputStreamBody.java b/jooby/src/main/java/io/jooby/internal/InputStreamBody.java
index 3ab5fae17..e130f9a8c 100644
--- a/jooby/src/main/java/io/jooby/internal/InputStreamBody.java
+++ b/jooby/src/main/java/io/jooby/internal/InputStreamBody.java
@@ -17,7 +17,6 @@ import java.util.List;
 import java.util.Map;
 
 import edu.umd.cs.findbugs.annotations.NonNull;
-import edu.umd.cs.findbugs.annotations.Nullable;
 import io.jooby.Body;
 import io.jooby.Context;
 import io.jooby.MediaType;
@@ -96,11 +95,6 @@ public class InputStreamBody implements Body {
     return ctx.decode(type, ctx.getRequestType(MediaType.text));
   }
 
-  @Nullable @Override
-  public <T> T toNullable(@NonNull Type type) {
-    return ctx.decode(type, ctx.getRequestType(MediaType.text));
-  }
-
   @NonNull @Override
   public List<String> toList() {
     return Collections.singletonList(value());
diff --git a/jooby/src/main/java/io/jooby/internal/MissingValue.java b/jooby/src/main/java/io/jooby/internal/MissingValue.java
index 36990cde7..9b4ad0839 100644
--- a/jooby/src/main/java/io/jooby/internal/MissingValue.java
+++ b/jooby/src/main/java/io/jooby/internal/MissingValue.java
@@ -13,7 +13,6 @@ import java.util.Optional;
 import java.util.Set;
 
 import edu.umd.cs.findbugs.annotations.NonNull;
-import edu.umd.cs.findbugs.annotations.Nullable;
 import io.jooby.ValueNode;
 import io.jooby.exception.MissingValueException;
 
@@ -44,11 +43,6 @@ public class MissingValue implements ValueNode {
     return null;
   }
 
-  @Nullable @Override
-  public <T> T toNullable(@NonNull Class<T> type) {
-    return null;
-  }
-
   @Override
   public String value() {
     throw new MissingValueException(name);
diff --git a/jooby/src/main/java/io/jooby/internal/QueryStringValue.java b/jooby/src/main/java/io/jooby/internal/QueryStringValue.java
index 366262c16..b4464bf06 100644
--- a/jooby/src/main/java/io/jooby/internal/QueryStringValue.java
+++ b/jooby/src/main/java/io/jooby/internal/QueryStringValue.java
@@ -17,19 +17,6 @@ public class QueryStringValue extends HashValue implements QueryString {
     this.queryString = queryString;
   }
 
-  protected boolean allowEmptyBean() {
-    return true;
-  }
-
-  @Override
-  protected <T> T toNullable(@NonNull Context ctx, @NonNull Class<T> type, boolean allowEmpty) {
-    // NOTE: 2.x backward compatible. Make sure Query object are almost always created
-    // GET /search?
-    // with class Search (q="*")
-    // so q is defaulted to "*"
-    return ValueConverters.convert(this, type, ctx.getRouter(), allowEmpty);
-  }
-
   @NonNull @Override
   public String queryString() {
     return queryString;
diff --git a/jooby/src/main/java/io/jooby/internal/RouterImpl.java b/jooby/src/main/java/io/jooby/internal/RouterImpl.java
index 2a7f20aae..ca4c2966b 100644
--- a/jooby/src/main/java/io/jooby/internal/RouterImpl.java
+++ b/jooby/src/main/java/io/jooby/internal/RouterImpl.java
@@ -564,6 +564,7 @@ public class RouterImpl implements Router {
 
     // Must be last, as fallback
     ValueConverter.addFallbackConverters(converters);
+    BeanConverter.addFallbackConverters(beanConverters);
 
     ExecutionMode mode = app.getExecutionMode();
     for (Route route : routes) {
diff --git a/jooby/src/main/java/io/jooby/internal/SingleValue.java b/jooby/src/main/java/io/jooby/internal/SingleValue.java
index a8260ab01..d3db74d99 100644
--- a/jooby/src/main/java/io/jooby/internal/SingleValue.java
+++ b/jooby/src/main/java/io/jooby/internal/SingleValue.java
@@ -17,7 +17,6 @@ import java.util.Optional;
 import java.util.Set;
 
 import edu.umd.cs.findbugs.annotations.NonNull;
-import edu.umd.cs.findbugs.annotations.Nullable;
 import io.jooby.Context;
 import io.jooby.ValueNode;
 
@@ -90,11 +89,6 @@ public class SingleValue implements ValueNode {
     return ctx.convert(this, type);
   }
 
-  @Nullable @Override
-  public <T> T toNullable(@NonNull Class<T> type) {
-    return ctx.convertOrNull(this, type);
-  }
-
   @Override
   public Map<String, List<String>> toMultimap() {
     return singletonMap(name, singletonList(value));
diff --git a/jooby/src/main/java/io/jooby/internal/ValueConverters.java b/jooby/src/main/java/io/jooby/internal/ValueConverters.java
index b41f7f25f..64d8f826b 100644
--- a/jooby/src/main/java/io/jooby/internal/ValueConverters.java
+++ b/jooby/src/main/java/io/jooby/internal/ValueConverters.java
@@ -26,7 +26,6 @@ import io.jooby.internal.converter.InstantConverter;
 import io.jooby.internal.converter.LocalDateConverter;
 import io.jooby.internal.converter.LocalDateTimeConverter;
 import io.jooby.internal.converter.PeriodConverter;
-import io.jooby.internal.converter.ReflectiveBeanConverter;
 import io.jooby.internal.converter.StatusCodeConverter;
 import io.jooby.internal.converter.TimeZoneConverter;
 import io.jooby.internal.converter.URIConverter;
@@ -74,10 +73,10 @@ public class ValueConverters {
     if (Optional.class.isAssignableFrom(rawType)) {
       return (T) Optional.ofNullable(convert(value, $Types.parameterizedType0(type), router));
     }
-    return convert(value, rawType, router, false);
+    return convert(value, rawType, router);
   }
 
-  public static <T> T convert(ValueNode value, Class type, Router router, boolean allowEmptyBean) {
+  public static <T> T convert(ValueNode value, Class type, Router router) {
     if (type == String.class) {
       return (T) value.valueOrNull();
     }
@@ -132,9 +131,7 @@ public class ValueConverters {
         }
       }
     }
-    // Fallback:
-    ReflectiveBeanConverter reflective = new ReflectiveBeanConverter();
-    return (T) reflective.convert(value, type, allowEmptyBean);
+    return null;
   }
 
   private static Object enumValue(ValueNode value, Class type) {
diff --git a/jooby/src/main/java/io/jooby/internal/WebSocketMessageImpl.java b/jooby/src/main/java/io/jooby/internal/WebSocketMessageImpl.java
index bde99b437..eb81a1c92 100644
--- a/jooby/src/main/java/io/jooby/internal/WebSocketMessageImpl.java
+++ b/jooby/src/main/java/io/jooby/internal/WebSocketMessageImpl.java
@@ -8,7 +8,6 @@ package io.jooby.internal;
 import java.lang.reflect.Type;
 
 import edu.umd.cs.findbugs.annotations.NonNull;
-import edu.umd.cs.findbugs.annotations.Nullable;
 import io.jooby.Body;
 import io.jooby.Context;
 import io.jooby.DefaultContext;
@@ -57,9 +56,4 @@ public class WebSocketMessageImpl extends ByteArrayBody implements WebSocketMess
     MediaType contentType = ctx.getRoute().getConsumes().get(0);
     return new WebSocketMessageBody(ctx, this).decode(type, contentType);
   }
-
-  @Nullable @Override
-  public <T> T toNullable(@NonNull Type type) {
-    return this.to(type);
-  }
 }
diff --git a/jooby/src/main/java/io/jooby/internal/converter/FromStringConverter.java b/jooby/src/main/java/io/jooby/internal/converter/FromStringConverter.java
deleted file mode 100644
index 8bfd7f6ef..000000000
--- a/jooby/src/main/java/io/jooby/internal/converter/FromStringConverter.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Jooby https://jooby.io
- * Apache License Version 2.0 https://jooby.io/LICENSE.txt
- * Copyright 2014 Edgar Espina
- */
-package io.jooby.internal.converter;
-
-import java.lang.reflect.Executable;
-import java.lang.reflect.InvocationTargetException;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.function.BiFunction;
-
-import io.jooby.SneakyThrows;
-import io.jooby.Value;
-import io.jooby.ValueConverter;
-
-public abstract class FromStringConverter<E extends Executable>
-    implements ValueConverter, BiFunction<Class, E, E> {
-
-  private Map<Class, E> cache = new ConcurrentHashMap<>();
-
-  @Override
-  public boolean supports(Class type) {
-    return cache.compute(type, this) != null;
-  }
-
-  @Override
-  public Object convert(Value value, Class type) {
-    try {
-      return invoke(cache.compute(type, this), value.value());
-    } catch (InvocationTargetException x) {
-      throw SneakyThrows.propagate(x.getTargetException());
-    } catch (IllegalAccessException | InstantiationException x) {
-      throw SneakyThrows.propagate(x);
-    }
-  }
-
-  @Override
-  public E apply(Class type, E executable) {
-    if (executable == null) {
-      return mappingMethod(type);
-    }
-    return executable;
-  }
-
-  protected abstract Object invoke(E executable, String value)
-      throws InvocationTargetException, IllegalAccessException, InstantiationException;
-
-  protected abstract E mappingMethod(Class type);
-}
diff --git a/jooby/src/main/java/io/jooby/internal/converter/ReflectiveBeanConverter.java b/jooby/src/main/java/io/jooby/internal/converter/ReflectiveBeanConverter.java
index 8ef325813..55af2a7c6 100644
--- a/jooby/src/main/java/io/jooby/internal/converter/ReflectiveBeanConverter.java
+++ b/jooby/src/main/java/io/jooby/internal/converter/ReflectiveBeanConverter.java
@@ -14,21 +14,20 @@ import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
 import java.lang.reflect.Parameter;
-import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.LinkedHashSet;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
 import java.util.function.Consumer;
 
 import edu.umd.cs.findbugs.annotations.NonNull;
+import io.jooby.BeanConverter;
 import io.jooby.FileUpload;
 import io.jooby.Formdata;
 import io.jooby.Usage;
-import io.jooby.Value;
 import io.jooby.ValueNode;
 import io.jooby.exception.BadRequestException;
 import io.jooby.exception.MissingValueException;
@@ -37,24 +36,22 @@ import io.jooby.internal.reflect.$Types;
 import jakarta.inject.Inject;
 import jakarta.inject.Named;
 
-public class ReflectiveBeanConverter {
-
-  private record Setter(Method method, Object arg) {
-    public void invoke(Object instance) throws InvocationTargetException, IllegalAccessException {
-      method.invoke(instance, arg);
-    }
-  }
-  ;
-
+public class ReflectiveBeanConverter implements BeanConverter {
   private static final String AMBIGUOUS_CONSTRUCTOR =
       "Ambiguous constructor found. Expecting a single constructor or only one annotated with "
           + Inject.class.getName();
 
   private static final Object[] NO_ARGS = new Object[0];
 
-  public Object convert(@NonNull ValueNode node, @NonNull Class type, boolean allowEmptyBean) {
+  @Override
+  public boolean supports(@NonNull Class type) {
+    return true;
+  }
+
+  @Override
+  public Object convert(@NonNull ValueNode node, @NonNull Class type) {
     try {
-      return newInstance(type, node, allowEmptyBean);
+      return newInstance(type, node);
     } catch (InstantiationException | IllegalAccessException | NoSuchMethodException x) {
       throw propagate(x);
     } catch (InvocationTargetException x) {
@@ -62,30 +59,18 @@ public class ReflectiveBeanConverter {
     }
   }
 
-  private static Object newInstance(Class type, ValueNode node, boolean allowEmptyBean)
-      throws IllegalAccessException,
-          InstantiationException,
-          InvocationTargetException,
+  private static <T> T newInstance(Class<T> type, ValueNode node)
+      throws IllegalAccessException, InstantiationException, InvocationTargetException,
           NoSuchMethodException {
     Constructor[] constructors = type.getConstructors();
-    Set<ValueNode> state = new HashSet<>();
-    Constructor constructor;
     if (constructors.length == 0) {
-      constructor = type.getDeclaredConstructor();
-    } else {
-      constructor = selectConstructor(constructors);
+      return setters(type.getDeclaredConstructor().newInstance(), node, Collections.emptySet());
     }
+    Constructor constructor = selectConstructor(constructors);
+    Set<Object> state = new HashSet<>();
     Object[] args =
         constructor.getParameterCount() == 0 ? NO_ARGS : inject(node, constructor, state::add);
-    List<Setter> setters = setters(type, node, state);
-    if (!allowEmptyBean && state.stream().allMatch(Value::isMissing)) {
-      return null;
-    }
-    var instance = constructor.newInstance(args);
-    for (Setter setter : setters) {
-      setter.invoke(instance);
-    }
-    return instance;
+    return (T) setters(constructor.newInstance(args), node, state);
   }
 
   private static Constructor selectConstructor(Constructor[] constructors) {
@@ -164,29 +149,32 @@ public class ReflectiveBeanConverter {
     return names;
   }
 
-  private static List<Setter> setters(Class type, ValueNode node, Set<ValueNode> nodes) {
-    var methods = type.getMethods();
-    var result = new ArrayList<Setter>();
+  private static <T> T setters(T newInstance, ValueNode node, Set<Object> skip) {
+    Method[] methods = newInstance.getClass().getMethods();
     for (String name : names(node)) {
       ValueNode value = node.get(name);
-      if (nodes.add(value)) {
-        Method method = findSetter(methods, name);
+      if (!skip.contains(value)) {
+        String methodName = "set" + Character.toUpperCase(name.charAt(0)) + name.substring(1);
+        Method method = findSetter(methods, methodName);
+        if (method == null) {
+          method = findSetter(methods, name);
+        }
         if (method != null) {
           Parameter parameter = method.getParameters()[0];
           try {
             Object arg = value(parameter, node, value);
-            result.add(new Setter(method, arg));
+            method.invoke(newInstance, arg);
           } catch (ProvisioningException x) {
             throw x;
+          } catch (InvocationTargetException x) {
+            throw new ProvisioningException(parameter, x.getCause());
           } catch (Exception x) {
             throw new ProvisioningException(parameter, x);
           }
-        } else {
-          nodes.remove(value);
         }
       }
     }
-    return result;
+    return newInstance;
   }
 
   private static Object value(Parameter parameter, ValueNode node, ValueNode value) {
@@ -235,18 +223,11 @@ public class ReflectiveBeanConverter {
   }
 
   private static Method findSetter(Method[] methods, String name) {
-    var setter = "set" + Character.toUpperCase(name.charAt(0)) + name.substring(1);
-    var candidates = new LinkedList<Method>();
     for (Method method : methods) {
-      if ((method.getName().equals(name) || method.getName().equals(setter))
-          && method.getParameterCount() == 1) {
-        if (method.getName().startsWith("set")) {
-          candidates.addFirst(method);
-        } else {
-          candidates.addLast(method);
-        }
+      if (method.getName().equals(name) && method.getParameterCount() == 1) {
+        return method;
       }
     }
-    return candidates.isEmpty() ? null : candidates.getFirst();
+    return null;
   }
 }
diff --git a/jooby/src/main/java/io/jooby/internal/converter/StringConstructorConverter.java b/jooby/src/main/java/io/jooby/internal/converter/StringConstructorConverter.java
deleted file mode 100644
index 7932512ec..000000000
--- a/jooby/src/main/java/io/jooby/internal/converter/StringConstructorConverter.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Jooby https://jooby.io
- * Apache License Version 2.0 https://jooby.io/LICENSE.txt
- * Copyright 2014 Edgar Espina
- */
-package io.jooby.internal.converter;
-
-import java.lang.reflect.Constructor;
-import java.lang.reflect.InvocationTargetException;
-
-public class StringConstructorConverter extends FromStringConverter<Constructor> {
-  @Override
-  protected Object invoke(Constructor executable, String value)
-      throws InvocationTargetException, IllegalAccessException, InstantiationException {
-    return executable.newInstance(value);
-  }
-
-  @Override
-  protected Constructor mappingMethod(Class type) {
-    try {
-      return type.getDeclaredConstructor(String.class);
-    } catch (NoSuchMethodException e) {
-      return null;
-    }
-  }
-}
diff --git a/jooby/src/main/java/io/jooby/internal/converter/ValueOfConverter.java b/jooby/src/main/java/io/jooby/internal/converter/ValueOfConverter.java
index 1307d755a..7de19e654 100644
--- a/jooby/src/main/java/io/jooby/internal/converter/ValueOfConverter.java
+++ b/jooby/src/main/java/io/jooby/internal/converter/ValueOfConverter.java
@@ -8,25 +8,48 @@ package io.jooby.internal.converter;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.BiFunction;
 
-public class ValueOfConverter extends FromStringConverter<Method> {
+import io.jooby.SneakyThrows;
+import io.jooby.Value;
+import io.jooby.ValueConverter;
+
+public class ValueOfConverter implements ValueConverter, BiFunction<Class, Method, Method> {
+
+  private Map<Class, Method> cache = new ConcurrentHashMap<>();
 
   @Override
-  protected Object invoke(Method executable, String value)
-      throws InvocationTargetException, IllegalAccessException {
-    return executable.invoke(null, value);
+  public boolean supports(Class type) {
+    return cache.compute(type, this) != null;
   }
 
   @Override
-  protected Method mappingMethod(Class type) {
+  public Object convert(Value value, Class type) {
     try {
-      Method valueOf = type.getDeclaredMethod("valueOf", String.class);
-      if (Modifier.isStatic(valueOf.getModifiers()) && Modifier.isPublic(valueOf.getModifiers())) {
-        return valueOf;
+      return cache.compute(type, this).invoke(null, value.value());
+    } catch (InvocationTargetException x) {
+      throw SneakyThrows.propagate(x.getTargetException());
+    } catch (IllegalAccessException x) {
+      throw SneakyThrows.propagate(x);
+    }
+  }
+
+  @Override
+  public Method apply(Class type, Method method) {
+    if (method == null) {
+      try {
+        Method valueOf = type.getDeclaredMethod("valueOf", String.class);
+        if (Modifier.isStatic(valueOf.getModifiers())
+            && Modifier.isPublic(valueOf.getModifiers())) {
+          return valueOf;
+        }
+        return null;
+      } catch (NoSuchMethodException x) {
+        return null;
       }
-      return null;
-    } catch (NoSuchMethodException x) {
-      return null;
     }
+    return method;
   }
 }
diff --git a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MethodDescriptor.java b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MethodDescriptor.java
index 97eb9f6b4..95110e40e 100644
--- a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MethodDescriptor.java
+++ b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MethodDescriptor.java
@@ -194,11 +194,6 @@ public class MethodDescriptor {
       return new MethodDescriptor(
           JoobyTypes.ValueNode, "to", Class.class.getName(), Object.class.getName());
     }
-
-    public static MethodDescriptor toNullable() {
-      return new MethodDescriptor(
-          JoobyTypes.ValueNode, "toNullable", Class.class.getName(), Object.class.getName());
-    }
   }
 
   public static class FileUpload {
diff --git a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/ParamDefinition.java b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/ParamDefinition.java
index 9bd32fe45..a30667b8e 100644
--- a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/ParamDefinition.java
+++ b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/ParamDefinition.java
@@ -253,7 +253,7 @@ public class ParamDefinition {
           ? MethodDescriptor.Context.bodyClass()
           : MethodDescriptor.Context.bodyType();
     }
-    return isNullable() ? ValueNode.toNullable() : ValueNode.to();
+    return ValueNode.to();
   }
 
   public static ParamDefinition create(
diff --git a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/TypeDefinition.java b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/TypeDefinition.java
index e0176579a..e805350a1 100644
--- a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/TypeDefinition.java
+++ b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/TypeDefinition.java
@@ -140,38 +140,6 @@ public class TypeDefinition {
     return asmType(getName(typeUtils.erasure(type)));
   }
 
-  public Class<?> toClass() {
-    Type type = toJvmType();
-    String className = type.getClassName();
-    try {
-      if (!className.contains("[")) return Class.forName(className);
-      else return Class.forName(type.getDescriptor());
-    } catch (ClassNotFoundException e) {
-      switch (className) {
-        case "byte":
-          return byte.class;
-        case "short":
-          return short.class;
-        case "int":
-          return int.class;
-        case "long":
-          return long.class;
-        case "char":
-          return char.class;
-        case "float":
-          return float.class;
-        case "double":
-          return double.class;
-        case "boolean":
-          return boolean.class;
-        case "void":
-          return void.class;
-        default:
-          throw new IllegalArgumentException("The type didn't represent a class: " + className);
-      }
-    }
-  }
-
   public boolean isRawType() {
     return type.toString().equals(getRawType().toString());
   }
diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBody.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBody.java
index 2586dd47e..1107fd815 100644
--- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBody.java
+++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBody.java
@@ -19,7 +19,6 @@ import java.util.List;
 import java.util.Map;
 
 import edu.umd.cs.findbugs.annotations.NonNull;
-import edu.umd.cs.findbugs.annotations.Nullable;
 import io.jooby.Body;
 import io.jooby.Context;
 import io.jooby.MediaType;
@@ -103,11 +102,6 @@ public class NettyBody implements Body {
     return ctx.decode(type, ctx.getRequestType(MediaType.text));
   }
 
-  @Nullable @Override
-  public <T> T toNullable(@NonNull Type type) {
-    return ctx.decode(type, ctx.getRequestType(MediaType.text));
-  }
-
   @Override
   public Map<String, List<String>> toMultimap() {
     return Collections.emptyMap();
diff --git a/pom.xml b/pom.xml
index 158e2b357..a183e3864 100644
--- a/pom.xml
+++ b/pom.xml
@@ -38,7 +38,7 @@
     <mysql-connector-j.version>8.0.33</mysql-connector-j.version>
     <log4jdbc.version>1.2</log4jdbc.version>
     <hibernate.version>6.2.0.Final</hibernate.version>
-    <ebean.version>13.17.4-jakarta</ebean.version>
+    <ebean.version>13.18.0-jakarta</ebean.version>
     <jdbi.version>3.38.2</jdbi.version>
     <flyway.version>9.17.0</flyway.version>
     <graphql-java.version>20.2</graphql-java.version>


Test patch:
diff --git a/jooby/src/test/java/io/jooby/Issue2525.java b/jooby/src/test/java/io/jooby/Issue2525.java
deleted file mode 100644
index d4e5e2fe4..000000000
--- a/jooby/src/test/java/io/jooby/Issue2525.java
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * Jooby https://jooby.io
- * Apache License Version 2.0 https://jooby.io/LICENSE.txt
- * Copyright 2014 Edgar Espina
- */
-package io.jooby;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import java.util.Objects;
-import java.util.function.Consumer;
-
-import org.jetbrains.annotations.NotNull;
-import org.junit.jupiter.api.Test;
-
-import io.jooby.internal.UrlParser;
-import io.jooby.internal.ValueConverterHelper;
-import jakarta.inject.Inject;
-
-public class Issue2525 {
-
-  public class VC2525 implements ValueConverter {
-    @Override
-    public boolean supports(@NotNull Class type) {
-      return type == MyID2525.class;
-    }
-
-    @Override
-    public Object convert(@NotNull Value value, @NotNull Class type) {
-      return new MyID2525(value.value());
-    }
-  }
-
-  public class MyID2525 {
-
-    private String value;
-
-    @Inject
-    public MyID2525(String value) {
-      this.value = value;
-    }
-
-    @Override
-    public String toString() {
-      return "MyID:" + value;
-    }
-  }
-
-  public static class Foo2525 {
-    public Integer a;
-    public Integer b;
-
-    public Foo2525(Integer a, Integer b) {
-      Objects.requireNonNull(a);
-      Objects.requireNonNull(b);
-
-      this.a = a;
-      this.b = b;
-    }
-
-    @Override
-    public String toString() {
-      return "{" + "a=" + a + ", b=" + b + '}';
-    }
-  }
-
-  public static class JavaBeanParam {
-    private String foo;
-
-    public String getFoo() {
-      return foo;
-    }
-
-    public void setFoo(String foo) {
-      this.foo = foo;
-    }
-
-    @Override
-    public String toString() {
-      return foo;
-    }
-  }
-
-  @Test
-  public void shouldBeMoreClever() {
-    queryString(
-        "foo=1234",
-        queryString -> {
-          assertEquals("[1234]", queryString.toList(JavaBeanParam.class).toString());
-        });
-    queryString(
-        "something=else&foo[0][a]=10&foo[0][b]=20&[0][a]=30&[0][b]=40",
-        queryString -> {
-          assertEquals("[{a=30, b=40}]", queryString.toList(Foo2525.class).toString());
-          assertEquals("[{a=10, b=20}]", queryString.get("foo").toList(Foo2525.class).toString());
-        });
-    queryString(
-        "id=1234",
-        queryString -> {
-          assertEquals("MyID:1234", queryString.get("id").to(MyID2525.class).toString());
-        },
-        new VC2525());
-    queryString(
-        "a=1&b=2&foo.a=3&foo.b=4",
-        queryString -> {
-          assertEquals("{a=1, b=2}", queryString.to(Foo2525.class).toString());
-          assertEquals("{a=3, b=4}", queryString.get("foo").to(Foo2525.class).toString());
-        });
-    queryString(
-        "something=else&foo[0][a]=10&foo[0][b]=20",
-        queryString -> {
-          assertEquals("[]", queryString.toList(Foo2525.class).toString());
-          assertEquals("[{a=10, b=20}]", queryString.get("foo").toList(Foo2525.class).toString());
-        });
-    queryString(
-        "something=else",
-        queryString -> {
-          assertEquals("[]", queryString.toList(Foo2525.class).toString());
-          assertEquals("[]", queryString.get("foo").toList(Foo2525.class).toString());
-        });
-    queryString(
-        "",
-        queryString -> {
-          assertEquals("[]", queryString.toList(Foo2525.class).toString());
-          assertEquals("[]", queryString.get("foo").toList(Foo2525.class).toString());
-        });
-  }
-
-  private void queryString(
-      String queryString, Consumer<QueryString> consumer, ValueConverter... converter) {
-    consumer.accept(
-        UrlParser.queryString(ValueConverterHelper.testContext(converter), queryString));
-  }
-}
diff --git a/jooby/src/test/java/io/jooby/ValueToBeanTest.java b/jooby/src/test/java/io/jooby/ValueToBeanTest.java
index 63a1631d5..0aaa52d47 100644
--- a/jooby/src/test/java/io/jooby/ValueToBeanTest.java
+++ b/jooby/src/test/java/io/jooby/ValueToBeanTest.java
@@ -517,7 +517,7 @@ public class ValueToBeanTest {
   }
 
   @Test
-  public void constructorAndMixed() {
+  public void construnctorAndMixed() {
     queryString(
         "level=L1&children[0]level=L2",
         queryString -> {
diff --git a/jooby/src/test/java/io/jooby/internal/ValueConverterHelper.java b/jooby/src/test/java/io/jooby/internal/ValueConverterHelper.java
index 28c81bd16..899c73f2e 100644
--- a/jooby/src/test/java/io/jooby/internal/ValueConverterHelper.java
+++ b/jooby/src/test/java/io/jooby/internal/ValueConverterHelper.java
@@ -20,7 +20,6 @@ import io.jooby.Context;
 import io.jooby.Router;
 import io.jooby.ValueConverter;
 import io.jooby.ValueNode;
-import io.jooby.exception.TypeMismatchException;
 
 public class ValueConverterHelper {
 
@@ -32,25 +31,13 @@ public class ValueConverterHelper {
         .filter(it -> (it instanceof BeanConverter))
         .forEach(it -> beans.add((BeanConverter) it));
     ValueConverter.addFallbackConverters(simple);
+    BeanConverter.addFallbackConverters(beans);
 
     Context ctx = mock(Context.class);
     Router router = mock(Router.class);
     when(router.getConverters()).thenReturn(simple);
     when(router.getBeanConverters()).thenReturn(beans);
-    when(ctx.getRouter()).thenReturn(router);
     when(ctx.convert(any(), any()))
-        .then(
-            (Answer)
-                invocation -> {
-                  ValueNode value = invocation.getArgument(0);
-                  Class type = invocation.getArgument(1);
-                  var result = ValueConverters.convert(value, type, router);
-                  if (result == null) {
-                    throw new TypeMismatchException(value.name(), type);
-                  }
-                  return result;
-                });
-    when(ctx.convertOrNull(any(), any()))
         .then(
             (Answer)
                 invocation -> {
diff --git a/modules/jooby-apt/src/test/java/io/jooby/apt/MvcModuleCompilerRunner.java b/modules/jooby-apt/src/test/java/io/jooby/apt/MvcModuleCompilerRunner.java
index 3076311fa..dc74fef17 100644
--- a/modules/jooby-apt/src/test/java/io/jooby/apt/MvcModuleCompilerRunner.java
+++ b/modules/jooby-apt/src/test/java/io/jooby/apt/MvcModuleCompilerRunner.java
@@ -24,6 +24,7 @@ import org.objectweb.asm.util.ASMifier;
 import com.google.common.truth.Truth;
 import com.google.testing.compile.JavaFileObjects;
 import com.google.testing.compile.JavaSourcesSubjectFactory;
+import io.jooby.BeanConverter;
 import io.jooby.Extension;
 import io.jooby.Jooby;
 import io.jooby.MvcFactory;
@@ -91,6 +92,8 @@ public class MvcModuleCompilerRunner {
     Extension extension = factory.create(provider);
     Jooby application = new Jooby();
 
+    application.converter(BeanConverter.reflective());
+
     application.install(extension);
 
     Path services =
diff --git a/modules/jooby-apt/src/test/java/io/jooby/apt/TestHandler.java b/modules/jooby-apt/src/test/java/io/jooby/apt/TestHandler.java
new file mode 100644
index 000000000..a2d27945e
--- /dev/null
+++ b/modules/jooby-apt/src/test/java/io/jooby/apt/TestHandler.java
@@ -0,0 +1,24 @@
+/*
+ * Jooby https://jooby.io
+ * Apache License Version 2.0 https://jooby.io/LICENSE.txt
+ * Copyright 2014 Edgar Espina
+ */
+package io.jooby.apt;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import io.jooby.Context;
+import io.jooby.Route;
+
+public class TestHandler implements Route.Handler {
+
+  private Route.Handler next;
+
+  public TestHandler(Route.Handler next) {
+    this.next = next;
+  }
+
+  @NonNull @Override
+  public Object apply(@NonNull Context ctx) throws Exception {
+    return next.apply(ctx);
+  }
+}
diff --git a/modules/jooby-apt/src/test/java/io/jooby/internal/apt/MockContextHelper.java b/modules/jooby-apt/src/test/java/io/jooby/internal/apt/MockContextHelper.java
index 105cb60ef..b358b4e0e 100644
--- a/modules/jooby-apt/src/test/java/io/jooby/internal/apt/MockContextHelper.java
+++ b/modules/jooby-apt/src/test/java/io/jooby/internal/apt/MockContextHelper.java
@@ -27,6 +27,7 @@ public class MockContextHelper {
         .filter(it -> (it instanceof BeanConverter))
         .forEach(it -> beans.add((BeanConverter) it));
     ValueConverter.addFallbackConverters(simple);
+    BeanConverter.addFallbackConverters(beans);
 
     Router router = mock(Router.class);
     when(router.getConverters()).thenReturn(simple);
diff --git a/modules/jooby-apt/src/test/java/tests/i2325/Issue2325.java b/modules/jooby-apt/src/test/java/tests/i2325/Issue2325.java
index b86b2282e..ff100a147 100644
--- a/modules/jooby-apt/src/test/java/tests/i2325/Issue2325.java
+++ b/modules/jooby-apt/src/test/java/tests/i2325/Issue2325.java
@@ -29,7 +29,7 @@ public class Issue2325 {
   }
 
   @Test
-  public void shouldNotFavorObjectConverterWhenNamedArgIsMissing() throws Exception {
+  public void shouldFavorObjectConverterWhenNamedArgIsMissing() throws Exception {
     new MvcModuleCompilerRunner(new C2325())
         .example(Expected2325.class)
         .module(
diff --git a/modules/jooby-apt/src/test/java/tests/i2525/C2525.java b/modules/jooby-apt/src/test/java/tests/i2525/C2525.java
deleted file mode 100644
index 176a4327c..000000000
--- a/modules/jooby-apt/src/test/java/tests/i2525/C2525.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Jooby https://jooby.io
- * Apache License Version 2.0 https://jooby.io/LICENSE.txt
- * Copyright 2014 Edgar Espina
- */
-package tests.i2525;
-
-import java.util.List;
-
-import io.jooby.annotation.GET;
-import io.jooby.annotation.QueryParam;
-
-public class C2525 {
-  @GET("/2525")
-  public String something(@QueryParam List<Foo2525> foo) {
-    return foo.toString();
-  }
-
-  @GET("/2325/bean")
-  public String getMy(@QueryParam MyID2525 myId) {
-    return myId.toString();
-  }
-}
diff --git a/modules/jooby-apt/src/test/java/tests/i2525/Expected2525.java b/modules/jooby-apt/src/test/java/tests/i2525/Expected2525.java
deleted file mode 100644
index 85461a445..000000000
--- a/modules/jooby-apt/src/test/java/tests/i2525/Expected2525.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Jooby https://jooby.io
- * Apache License Version 2.0 https://jooby.io/LICENSE.txt
- * Copyright 2014 Edgar Espina
- */
-package tests.i2525;
-
-import io.jooby.Extension;
-import io.jooby.Jooby;
-import io.jooby.MvcFactory;
-import jakarta.inject.Provider;
-
-public class Expected2525 implements MvcFactory {
-
-  private static void install(Jooby application, Provider<C2525> provider) throws Exception {
-    application.get(
-        "/2525",
-        ctx -> {
-          C2525 myController = provider.get();
-          return null;
-        });
-  }
-
-  @Override
-  public boolean supports(Class type) {
-    return type == C2525.class;
-  }
-
-  @Override
-  public Extension create(Provider provider) {
-    return app -> install(app, provider);
-  }
-}
diff --git a/modules/jooby-apt/src/test/java/tests/i2525/Foo2525.java b/modules/jooby-apt/src/test/java/tests/i2525/Foo2525.java
deleted file mode 100644
index b333dff3f..000000000
--- a/modules/jooby-apt/src/test/java/tests/i2525/Foo2525.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Jooby https://jooby.io
- * Apache License Version 2.0 https://jooby.io/LICENSE.txt
- * Copyright 2014 Edgar Espina
- */
-package tests.i2525;
-
-import java.util.Objects;
-
-public class Foo2525 {
-  public Integer a;
-  public Integer b;
-
-  public Foo2525(Integer a, Integer b) {
-    Objects.requireNonNull(a);
-    Objects.requireNonNull(b);
-
-    this.a = a;
-    this.b = b;
-  }
-
-  @Override
-  public String toString() {
-    return "{" + "a:" + a + ", b:" + b + '}';
-  }
-}
diff --git a/modules/jooby-apt/src/test/java/tests/i2525/Issue2525.java b/modules/jooby-apt/src/test/java/tests/i2525/Issue2525.java
deleted file mode 100644
index e75c8f34a..000000000
--- a/modules/jooby-apt/src/test/java/tests/i2525/Issue2525.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Jooby https://jooby.io
- * Apache License Version 2.0 https://jooby.io/LICENSE.txt
- * Copyright 2014 Edgar Espina
- */
-package tests.i2525;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import org.junit.jupiter.api.Test;
-
-import io.jooby.Context;
-import io.jooby.apt.MvcModuleCompilerRunner;
-import io.jooby.test.MockContext;
-import io.jooby.test.MockRouter;
-
-public class Issue2525 {
-  @Test
-  public void shouldAcceptEmptyList() throws Exception {
-    new MvcModuleCompilerRunner(new C2525())
-        .example(Expected2525.class)
-        .module(
-            app -> {
-              var router = new MockRouter(app);
-              var ctx = withQueryString("?");
-              var list = ctx.query().toList(Foo2525.class);
-              assertEquals("[]", router.get("/2525", withQueryString("?")).value());
-              assertEquals("[]", router.get("/2525", withQueryString("?something=else")).value());
-              assertEquals(
-                  "[{a:10, b:20}, {a:30, b:40}]",
-                  router
-                      .get(
-                          "/2525",
-                          withQueryString("?foo[0][a]=10&foo[0][b]=20&foo[1][a]=30&foo[1][b]=40"))
-                      .value());
-            });
-  }
-
-  private Context withQueryString(String queryString) {
-    return new MockContext().setQueryString(queryString);
-  }
-}
diff --git a/modules/jooby-apt/src/test/java/tests/i2525/MyID2525.java b/modules/jooby-apt/src/test/java/tests/i2525/MyID2525.java
deleted file mode 100644
index 504191809..000000000
--- a/modules/jooby-apt/src/test/java/tests/i2525/MyID2525.java
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Jooby https://jooby.io
- * Apache License Version 2.0 https://jooby.io/LICENSE.txt
- * Copyright 2014 Edgar Espina
- */
-package tests.i2525;
-
-import java.util.Optional;
-
-public class MyID2525 {
-
-  private String value;
-
-  public MyID2525(String value) {
-    this.value = value;
-  }
-
-  @Override
-  public String toString() {
-    return "MyID:" + Optional.ofNullable(value).orElse("{}");
-  }
-}
diff --git a/modules/jooby-apt/src/test/java/tests/i2525/VC2525.java b/modules/jooby-apt/src/test/java/tests/i2525/VC2525.java
deleted file mode 100644
index 02bd272c7..000000000
--- a/modules/jooby-apt/src/test/java/tests/i2525/VC2525.java
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Jooby https://jooby.io
- * Apache License Version 2.0 https://jooby.io/LICENSE.txt
- * Copyright 2014 Edgar Espina
- */
-package tests.i2525;
-
-import io.jooby.Value;
-import io.jooby.ValueConverter;
-import tests.i2325.MyID2325;
-
-public class VC2525 implements ValueConverter {
-  @Override
-  public boolean supports(Class type) {
-    return type == MyID2325.class;
-  }
-
-  @Override
-  public Object convert(Value value, Class type) {
-    return new MyID2325(value.value());
-  }
-}
diff --git a/tests/src/test/java/io/jooby/i2325/Issue2325.java b/tests/src/test/java/io/jooby/i2325/Issue2325.java
index 99c1fc7ae..d3eff02f5 100644
--- a/tests/src/test/java/io/jooby/i2325/Issue2325.java
+++ b/tests/src/test/java/io/jooby/i2325/Issue2325.java
@@ -41,7 +41,7 @@ public class Issue2325 {
   }
 
   @ServerTest
-  public void shouldWorkWithDefaultCustomConverter(ServerTestRunner runner) {
+  public void shouldFailToConvertWithoutCustomConverter(ServerTestRunner runner) {
     runner
         .define(
             app -> {
@@ -58,7 +58,9 @@ public class Issue2325 {
               http.get(
                   "/2325?id=" + uuid,
                   rsp -> {
-                    assertEquals("MyID:" + uuid, rsp.body().string());
+                    assertEquals(
+                        "Cannot convert value: 'id', to: '" + MyID2325.class.getName() + "'",
+                        rsp.body().string());
                   });
             });
   }
diff --git a/tests/src/test/java/io/jooby/i2525/Controller2525.java b/tests/src/test/java/io/jooby/i2525/Controller2525.java
deleted file mode 100644
index f2c7ab669..000000000
--- a/tests/src/test/java/io/jooby/i2525/Controller2525.java
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
- * Jooby https://jooby.io
- * Apache License Version 2.0 https://jooby.io/LICENSE.txt
- * Copyright 2014 Edgar Espina
- */
-package io.jooby.i2525;
-
-import java.util.List;
-
-import io.jooby.annotation.GET;
-import io.jooby.annotation.QueryParam;
-
-public class Controller2525 {
-  @GET("/2525")
-  public String something(@QueryParam List<Foo2525> foo) {
-    return foo.toString();
-  }
-}
diff --git a/tests/src/test/java/io/jooby/i2525/Foo2525.java b/tests/src/test/java/io/jooby/i2525/Foo2525.java
deleted file mode 100644
index a351c1022..000000000
--- a/tests/src/test/java/io/jooby/i2525/Foo2525.java
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Jooby https://jooby.io
- * Apache License Version 2.0 https://jooby.io/LICENSE.txt
- * Copyright 2014 Edgar Espina
- */
-package io.jooby.i2525;
-
-import java.util.Objects;
-
-public class Foo2525 {
-  public Integer a;
-  public Integer b;
-
-  public Foo2525(Integer a, Integer b) {
-    Objects.requireNonNull(a);
-    Objects.requireNonNull(b);
-
-    this.a = a;
-    this.b = b;
-  }
-}
diff --git a/tests/src/test/java/io/jooby/i2525/Issue2525.java b/tests/src/test/java/io/jooby/i2525/Issue2525.java
deleted file mode 100644
index 243a6a1a0..000000000
--- a/tests/src/test/java/io/jooby/i2525/Issue2525.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Jooby https://jooby.io
- * Apache License Version 2.0 https://jooby.io/LICENSE.txt
- * Copyright 2014 Edgar Espina
- */
-package io.jooby.i2525;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import io.jooby.jackson.JacksonModule;
-import io.jooby.junit.ServerTest;
-import io.jooby.junit.ServerTestRunner;
-
-public class Issue2525 {
-  @ServerTest
-  public void shouldHandleMultipleAcceptHeaders(ServerTestRunner runner) {
-    runner
-        .define(
-            app -> {
-              app.install(new JacksonModule());
-              app.get(
-                  "/2525",
-                  ctx -> {
-                    return ctx.query("foo").toList(Foo2525.class);
-                  });
-            })
-        .ready(
-            http -> {
-              http.get(
-                  "/2525",
-                  rsp -> {
-                    assertEquals("[]", rsp.body().string());
-                  });
-              http.get(
-                  "/2525?something=else",
-                  rsp -> {
-                    assertEquals("[]", rsp.body().string());
-                  });
-              http.get(
-                  "/2525?foo[0][a]=10&foo[0][b]=20&foo[1][a]=30&foo[1][b]=40&something=else",
-                  rsp -> {
-                    assertEquals("[{\"a\":10,\"b\":20},{\"a\":30,\"b\":40}]", rsp.body().string());
-                  });
-            });
-  }
-}
diff --git a/tests/src/test/kotlin/io/jooby/FeaturedKotlinTest.kt b/tests/src/test/kotlin/io/jooby/FeaturedKotlinTest.kt
index b7be6b3a1..0d3bda5c0 100644
--- a/tests/src/test/kotlin/io/jooby/FeaturedKotlinTest.kt
+++ b/tests/src/test/kotlin/io/jooby/FeaturedKotlinTest.kt
@@ -18,7 +18,6 @@ import kotlinx.coroutines.delay
 import org.json.JSONObject
 import org.junit.jupiter.api.Assertions.assertEquals
 import org.junit.jupiter.api.Assertions.assertNotEquals
-import org.junit.jupiter.api.Assertions.assertTrue
 
 class FeaturedKotlinTest {
 
@@ -101,13 +100,7 @@ class FeaturedKotlinTest {
         }
 
         client.get("/kotlin/point") { rsp ->
-          assertTrue(
-            rsp.body!!
-              .string()
-              .contains(
-                "Cannot convert value: &apos;null&apos;, to: &apos;io.jooby.internal.mvc.QueryPoint&apos;"
-              )
-          )
+          assertEquals("QueryPoint(x=null, y=null) : null", rsp.body!!.string())
         }
 
         client.get("/kotlin/point?x=9") { rsp ->