Skip to content

Commit d515d34

Browse files
oschwaldclaude
andcommitted
Fix enum pointer decoding with @MaxMindDbCreator
When an enum with @MaxMindDbCreator was accessed via a pointer in the database, decoding failed with ConstructorNotFoundException. This affected enums like ConnectionType in GeoIP2 when data was deduplicated via pointers. The bug was in requiresLookupContext() which called loadConstructorMetadata() before checking for creator methods. Enums don't have usable public constructors, causing the exception. Fix: - Add cls.isEnum() to early-return checks (zero overhead for enums) - Add @MaxMindDbCreator check for non-enum classes with creators - Add negative caching to getCachedCreator() to avoid repeated method scanning for classes without creators Fixes: maxmind/GeoIP2-java#644 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 01cd2d0 commit d515d34

File tree

3 files changed

+126
-3
lines changed

3 files changed

+126
-3
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
CHANGELOG
22
=========
33

4+
4.0.2
5+
------------------
6+
7+
* Fixed a bug where enums with `@MaxMindDbCreator` would throw
8+
`ConstructorNotFoundException` when the data was stored via a pointer
9+
in the database. This commonly occurred with deduplicated data in larger
10+
databases. Reported by Fabrice Bacchella. GitHub #644 in GeoIP2-java.
11+
412
4.0.1 (2025-12-02)
513
------------------
614

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ class Decoder {
3030

3131
private static final int[] POINTER_VALUE_OFFSETS = {0, 0, 1 << 11, (1 << 19) + (1 << 11), 0};
3232

33+
// Sentinel to cache "no creator method exists" to avoid repeated method scanning
34+
private static final CachedCreator NO_CREATOR = new CachedCreator(null, null);
35+
3336
private final NodeCache cache;
3437

3538
private final long pointerBase;
@@ -184,10 +187,17 @@ private boolean requiresLookupContext(Class<?> cls) {
184187
|| cls.equals(Object.class)
185188
|| Map.class.isAssignableFrom(cls)
186189
|| List.class.isAssignableFrom(cls)
190+
|| cls.isEnum()
187191
|| isSimpleType(cls)) {
188192
return false;
189193
}
190194

195+
// Non-enum classes with @MaxMindDbCreator don't require lookup context
196+
// since they just convert simple values (strings, booleans, etc.)
197+
if (getCachedCreator(cls) != null) {
198+
return false;
199+
}
200+
191201
var cached = getCachedConstructor(cls);
192202
if (cached == null) {
193203
cached = loadConstructorMetadata(cls);
@@ -960,14 +970,15 @@ private Object convertValue(Object value, Class<?> targetType) {
960970

961971
private CachedCreator getCachedCreator(Class<?> cls) {
962972
CachedCreator cached = this.creators.get(cls);
973+
if (cached == NO_CREATOR) {
974+
return null; // Known to have no creator
975+
}
963976
if (cached != null) {
964977
return cached;
965978
}
966979

967980
CachedCreator creator = findCreatorMethod(cls);
968-
if (creator != null) {
969-
this.creators.putIfAbsent(cls, creator);
970-
}
981+
this.creators.putIfAbsent(cls, creator != null ? creator : NO_CREATOR);
971982
return creator;
972983
}
973984

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

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2042,6 +2042,110 @@ private void testIpV6(Reader reader, File file) throws IOException {
20422042
}
20432043
}
20442044

2045+
// =========================================================================
2046+
// Tests for enum with @MaxMindDbCreator when data is stored via pointer
2047+
// See: https://github.com/maxmind/GeoIP2-java/issues/644
2048+
// =========================================================================
2049+
2050+
/**
2051+
* Enum with @MaxMindDbCreator for converting string values.
2052+
* This simulates how ConnectionType enum works in geoip2.
2053+
*/
2054+
enum ConnectionTypeEnum {
2055+
DIALUP("Dialup"),
2056+
CABLE_DSL("Cable/DSL"),
2057+
CORPORATE("Corporate"),
2058+
CELLULAR("Cellular"),
2059+
SATELLITE("Satellite"),
2060+
UNKNOWN("Unknown");
2061+
2062+
private final String name;
2063+
2064+
ConnectionTypeEnum(String name) {
2065+
this.name = name;
2066+
}
2067+
2068+
@MaxMindDbCreator
2069+
public static ConnectionTypeEnum fromString(String s) {
2070+
if (s == null) {
2071+
return UNKNOWN;
2072+
}
2073+
return switch (s) {
2074+
case "Dialup" -> DIALUP;
2075+
case "Cable/DSL" -> CABLE_DSL;
2076+
case "Corporate" -> CORPORATE;
2077+
case "Cellular" -> CELLULAR;
2078+
case "Satellite" -> SATELLITE;
2079+
default -> UNKNOWN;
2080+
};
2081+
}
2082+
}
2083+
2084+
/**
2085+
* Model class that uses the ConnectionTypeEnum for the connection_type field.
2086+
*/
2087+
static class TraitsModel {
2088+
ConnectionTypeEnum connectionType;
2089+
2090+
@MaxMindDbConstructor
2091+
public TraitsModel(
2092+
@MaxMindDbParameter(name = "connection_type")
2093+
ConnectionTypeEnum connectionType
2094+
) {
2095+
this.connectionType = connectionType;
2096+
}
2097+
}
2098+
2099+
/**
2100+
* Top-level model for Enterprise database records.
2101+
*/
2102+
static class EnterpriseModel {
2103+
TraitsModel traits;
2104+
2105+
@MaxMindDbConstructor
2106+
public EnterpriseModel(
2107+
@MaxMindDbParameter(name = "traits")
2108+
TraitsModel traits
2109+
) {
2110+
this.traits = traits;
2111+
}
2112+
}
2113+
2114+
/**
2115+
* This test passes because IP 74.209.24.0 has connection_type stored inline.
2116+
*/
2117+
@ParameterizedTest
2118+
@MethodSource("chunkSizes")
2119+
public void testEnumCreatorWithInlineData(int chunkSize) throws IOException {
2120+
try (var reader = new Reader(getFile("GeoIP2-Enterprise-Test.mmdb"), chunkSize)) {
2121+
var ip = InetAddress.getByName("74.209.24.0");
2122+
var result = reader.get(ip, EnterpriseModel.class);
2123+
assertNotNull(result);
2124+
assertNotNull(result.traits);
2125+
assertEquals(ConnectionTypeEnum.CABLE_DSL, result.traits.connectionType);
2126+
}
2127+
}
2128+
2129+
/**
2130+
* This test verifies that enums with @MaxMindDbCreator work correctly when
2131+
* the data is stored via a pointer (common for deduplication in databases).
2132+
*
2133+
* <p>Previously, this would throw ConstructorNotFoundException because
2134+
* requiresLookupContext() called loadConstructorMetadata() before checking
2135+
* for creator methods.
2136+
*/
2137+
@ParameterizedTest
2138+
@MethodSource("chunkSizes")
2139+
public void testEnumCreatorWithPointerData(int chunkSize) throws IOException {
2140+
try (var reader = new Reader(getFile("GeoIP2-Enterprise-Test.mmdb"), chunkSize)) {
2141+
var ip = InetAddress.getByName("89.160.20.112");
2142+
var result = reader.get(ip, EnterpriseModel.class);
2143+
assertNotNull(result);
2144+
assertNotNull(result.traits);
2145+
assertEquals(ConnectionTypeEnum.CORPORATE, result.traits.connectionType);
2146+
}
2147+
}
2148+
20452149
static File getFile(String name) {
20462150
return new File(ReaderTest.class.getResource("/maxmind-db/test-data/" + name).getFile());
20472151
}

0 commit comments

Comments
 (0)