Repository: jooby-project/jooby
Pull request #2955 (2023-06-05 01:01:44)

build(deps-dev): bump truth from 1.1.3 to 1.1.4
Bumps [truth](https://github.com/google/truth) from 1.1.3 to 1.1.4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/google/truth/releases">truth's releases</a>.</em></p>
<blockquote>
<h2>1.1.4</h2>
<ul>
<li>Updated Truth to build with <code>-source 8 -target 8</code>. This means that it no longer runs under Java 7 VMs. It continues to run under Android, even old versions, for all apps that have <a href="https://developer.android.com/studio/write/java8-support#supported_features">enabled support for Java 8 language features</a>. (db5db2429)</li>
<li>Updated Truth to depend on Guava 32.0.0. That release contains changes related to CVEs. Neither of the CVEs relates to any methods that are used by Truth, so this version bump is just about eliminating any warnings related to the old version and helping tools like Maven to select the newest version of Guava. (f8d4dbba8adc65effba70879d59a39da092dce51, 99b1df8852a25b5638590bea1b55a31ae536936d)</li>
<li>Added support for <code>value of: method()</code> to <code>expect.that</code>, matching the existing support for <code>assertThat</code>. (bd8efd003)</li>
<li>Enhanced <code>IterableSubject.containsAtLeastElementsIn().inOrder()</code> to print an extra line that shows only the expected elements in their actual order. (9da7dd184)</li>
<li>Annotated Truth for nullness. (2151add71)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a href="https://github.com/google/truth/commits/v1.1.4">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=com.google.truth:truth&package-manager=maven&previous-version=1.1.3&new-version=1.1.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

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..09d85063b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -117,7 +117,7 @@
 
     <auto-service.version>1.0.1</auto-service.version>
     <compile-testing.version>0.21.0</compile-testing.version>
-    <truth.version>1.1.3</truth.version>
+    <truth.version>1.1.4</truth.version>
 
     <!-- Gradle plugins -->
     <gradle-core.version>2.6</gradle-core.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 ->