Skip to content

Commit 0dbb03c

Browse files
oschwaldclaude
andcommitted
Improve error messages for constructor argument mismatches
When constructor invocation fails with IllegalArgumentException, the error diagnostic now: 1. Reports null-to-primitive issues: "null value for primitive double parameter 'ip_risk'" - this identifies the actual root cause when a database field is missing and the model uses a primitive type. 2. Uses proper type compatibility checks for boxed/primitive pairs. Previously, Boolean/boolean pairs were incorrectly reported as type mismatches even though auto-unboxing handles them correctly. This makes debugging much easier by pointing to the actual problem instead of reporting misleading type mismatches. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 3333630 commit 0dbb03c

File tree

3 files changed

+97
-6
lines changed

3 files changed

+97
-6
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ CHANGELOG
88
`ConstructorNotFoundException` when the data was stored via a pointer
99
in the database. This commonly occurred with deduplicated data in larger
1010
databases. Reported by Fabrice Bacchella. GitHub #644 in GeoIP2-java.
11+
* Improved error messages when constructor invocation fails. The error now
12+
correctly identifies null values being passed to primitive parameters
13+
instead of reporting misleading boxed/primitive type mismatches.
1114

1215
4.0.1 (2025-12-02)
1316
------------------

src/main/java/com/maxmind/db/Decoder.java

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -706,18 +706,68 @@ private <T> Object decodeMapIntoObject(int size, Class<T> cls)
706706
var sbErrors = new StringBuilder();
707707
for (var key : parameterIndexes.keySet()) {
708708
var index = parameterIndexes.get(key);
709-
if (parameters[index] != null
710-
&& !parameters[index].getClass().isAssignableFrom(parameterTypes[index])) {
711-
sbErrors.append(" argument type mismatch in " + key + " MMDB Type: "
712-
+ parameters[index].getClass().getCanonicalName()
713-
+ " Java Type: " + parameterTypes[index].getCanonicalName());
709+
if (parameters[index] == null && parameterTypes[index].isPrimitive()) {
710+
sbErrors.append(" null value for primitive ")
711+
.append(parameterTypes[index].getName())
712+
.append(" parameter '").append(key).append("'.");
713+
} else if (parameters[index] != null
714+
&& !isAssignableType(parameters[index].getClass(), parameterTypes[index])) {
715+
sbErrors.append(" argument type mismatch in ").append(key)
716+
.append(" MMDB Type: ")
717+
.append(parameters[index].getClass().getCanonicalName())
718+
.append(" Java Type: ")
719+
.append(parameterTypes[index].getCanonicalName())
720+
.append(".");
714721
}
715722
}
716723
throw new DeserializationException(
717-
"Error creating object of type: " + cls.getSimpleName() + " - " + sbErrors, e);
724+
"Error creating object of type: " + cls.getSimpleName() + " -" + sbErrors, e);
718725
}
719726
}
720727

728+
/**
729+
* Checks if an actual type can be assigned to an expected type,
730+
* accounting for primitive/boxed equivalents (e.g., Boolean to boolean).
731+
*/
732+
private static boolean isAssignableType(Class<?> actual, Class<?> expected) {
733+
if (expected.isAssignableFrom(actual)) {
734+
return true;
735+
}
736+
// Check primitive/boxed equivalents for auto-unboxing
737+
if (expected.isPrimitive()) {
738+
return actual.equals(boxedType(expected));
739+
}
740+
return false;
741+
}
742+
743+
private static Class<?> boxedType(Class<?> primitive) {
744+
if (primitive == boolean.class) {
745+
return Boolean.class;
746+
}
747+
if (primitive == byte.class) {
748+
return Byte.class;
749+
}
750+
if (primitive == short.class) {
751+
return Short.class;
752+
}
753+
if (primitive == int.class) {
754+
return Integer.class;
755+
}
756+
if (primitive == long.class) {
757+
return Long.class;
758+
}
759+
if (primitive == float.class) {
760+
return Float.class;
761+
}
762+
if (primitive == double.class) {
763+
return Double.class;
764+
}
765+
if (primitive == char.class) {
766+
return Character.class;
767+
}
768+
return primitive;
769+
}
770+
721771
private boolean shouldInstantiateFromContext(Class<?> parameterType) {
722772
if (parameterType == null
723773
|| parameterType.isPrimitive()

src/test/java/com/maxmind/db/ReaderTest.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2146,6 +2146,44 @@ public void testEnumCreatorWithPointerData(int chunkSize) throws IOException {
21462146
}
21472147
}
21482148

2149+
// Model class with primitive double field for testing null-to-primitive error messages
2150+
static class IpRiskModelWithPrimitive {
2151+
public final double ipRisk;
2152+
2153+
@MaxMindDbConstructor
2154+
public IpRiskModelWithPrimitive(
2155+
@MaxMindDbParameter(name = "ip_risk") double ipRisk
2156+
) {
2157+
this.ipRisk = ipRisk;
2158+
}
2159+
}
2160+
2161+
/**
2162+
* Tests that error messages correctly report null-to-primitive issues instead of
2163+
* misleading boxed/primitive type mismatch messages.
2164+
*
2165+
* <p>IP 11.1.2.3 in the IP Risk test database doesn't have an ip_risk field.
2166+
* When deserializing to a model with a primitive double, the error message should
2167+
* correctly identify "null value for primitive double" rather than reporting
2168+
* misleading Boolean/boolean mismatches.
2169+
*/
2170+
@ParameterizedTest
2171+
@MethodSource("chunkSizes")
2172+
public void testNullToPrimitiveErrorMessage(int chunkSize) throws IOException {
2173+
try (var reader = new Reader(getFile("GeoIP2-IP-Risk-Test.mmdb"), chunkSize)) {
2174+
var ip = InetAddress.getByName("11.1.2.3");
2175+
2176+
var exception = assertThrows(DeserializationException.class,
2177+
() -> reader.get(ip, IpRiskModelWithPrimitive.class));
2178+
2179+
// Error message should mention null value for primitive, not type mismatch
2180+
assertTrue(exception.getMessage().contains("null value for primitive double"),
2181+
"Error message should identify null-to-primitive issue: " + exception.getMessage());
2182+
assertTrue(exception.getMessage().contains("ip_risk"),
2183+
"Error message should name the problematic parameter: " + exception.getMessage());
2184+
}
2185+
}
2186+
21492187
static File getFile(String name) {
21502188
return new File(ReaderTest.class.getResource("/maxmind-db/test-data/" + name).getFile());
21512189
}

0 commit comments

Comments
 (0)