Skip to content

Commit 4b36416

Browse files
committed
Support fallback constructor/parameter selection
- Prefer @MaxMindDbConstructor; fallback to record canonical or single public constructor - Infer parameter names: annotation > record component > Java parameter when `-parameters` is set; throw ParameterNotFoundException if names unavailable - Enable -parameters in maven-compiler-plugin
1 parent a462282 commit 4b36416

File tree

5 files changed

+144
-14
lines changed

5 files changed

+144
-14
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ CHANGELOG
2222
accessor methods (e.g., `binaryFormatMajorVersion()`, `databaseType()`, etc.).
2323
* `Network.getNetworkAddress()` and `Network.getPrefixLength()` have been
2424
replaced with record accessor methods `networkAddress()` and `prefixLength()`.
25+
* Deserialization improvements:
26+
* If no constructor is annotated with `@MaxMindDbConstructor`, records now
27+
use their canonical constructor automatically. For non‑record classes with
28+
a single public constructor, that constructor is used by default.
29+
* `@MaxMindDbParameter` annotations are now optional when parameter names
30+
match field names in the database: for records, component names are used;
31+
for classes, Java parameter names are used (when compiled with
32+
`-parameters`). Annotations still take precedence when present.
2533

2634
3.2.0 (2025-05-28)
2735
------------------

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,23 @@ public class Lookup {
120120
}
121121
```
122122

123+
### Constructor and parameter selection
124+
125+
- Preferred: annotate a constructor with `@MaxMindDbConstructor` and its
126+
parameters with `@MaxMindDbParameter(name = "...")`.
127+
- Records: if no constructor is annotated, the canonical record constructor is
128+
used automatically. Record component names are used as field names.
129+
- Classes with a single public constructor: if no constructor is annotated,
130+
that constructor is used automatically.
131+
- Unannotated parameters: when a parameter is not annotated, the reader falls
132+
back to the parameter name. For records, this is the component name; for
133+
classes, this is the Java parameter name. To use Java parameter names at
134+
runtime, compile your model classes with the `-parameters` flag (Maven:
135+
`maven-compiler-plugin` with `<parameters>true</parameters>`).
136+
If Java parameter names are unavailable (no `-parameters`) and there is no
137+
`@MaxMindDbParameter` annotation, the reader throws a
138+
`ParameterNotFoundException` with guidance.
139+
123140
You can also use the reader object to iterate over the database.
124141
The `reader.networks()` and `reader.networksWithin()` methods can
125142
be used for this purpose.

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@
150150
<release>17</release>
151151
<source>17</source>
152152
<target>17</target>
153+
<parameters>true</parameters>
153154
</configuration>
154155
</plugin>
155156
<plugin>

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

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -388,8 +388,25 @@ private <T> Object decodeMapIntoObject(int size, Class<T> cls)
388388
parameterIndexes = new HashMap<>();
389389
var annotations = constructor.getParameterAnnotations();
390390
for (int i = 0; i < constructor.getParameterCount(); i++) {
391-
var parameterName = getParameterName(cls, i, annotations[i]);
392-
parameterIndexes.put(parameterName, i);
391+
var name = getParameterName(annotations[i]);
392+
if (name == null) {
393+
// Fallbacks: record component name, then Java parameter name
394+
// (requires -parameters)
395+
if (cls.isRecord()) {
396+
name = cls.getRecordComponents()[i].getName();
397+
} else {
398+
var param = constructor.getParameters()[i];
399+
if (param.isNamePresent()) {
400+
name = param.getName();
401+
} else {
402+
throw new ParameterNotFoundException(
403+
"Parameter name for index " + i + " on class " + cls.getName()
404+
+ " is not available. Annotate with @MaxMindDbParameter "
405+
+ "or compile with -parameters.");
406+
}
407+
}
408+
}
409+
parameterIndexes.put(name, i);
393410
}
394411

395412
this.constructors.put(
@@ -457,34 +474,56 @@ private <T> CachedConstructor<T> getCachedConstructor(Class<T> cls) {
457474
private static <T> Constructor<T> findConstructor(Class<T> cls)
458475
throws ConstructorNotFoundException {
459476
var constructors = cls.getConstructors();
477+
// Prefer explicitly annotated constructor
460478
for (var constructor : constructors) {
461479
if (constructor.getAnnotation(MaxMindDbConstructor.class) == null) {
462480
continue;
463481
}
464482
@SuppressWarnings("unchecked")
465-
Constructor<T> constructor2 = (Constructor<T>) constructor;
466-
return constructor2;
483+
var selected = (Constructor<T>) constructor;
484+
return selected;
467485
}
468486

469-
throw new ConstructorNotFoundException("No constructor on class " + cls.getName()
470-
+ " with the MaxMindDbConstructor annotation was found.");
487+
// Fallback for records: use canonical constructor
488+
if (cls.isRecord()) {
489+
try {
490+
var components = cls.getRecordComponents();
491+
var types = new Class<?>[components.length];
492+
for (int i = 0; i < components.length; i++) {
493+
types[i] = components[i].getType();
494+
}
495+
var c = cls.getDeclaredConstructor(types);
496+
@SuppressWarnings("unchecked")
497+
var selected = (Constructor<T>) c;
498+
return selected;
499+
} catch (NoSuchMethodException e) {
500+
// ignore and continue to next fallback
501+
}
502+
}
503+
504+
// Fallback for single-constructor classes
505+
if (constructors.length == 1) {
506+
var only = constructors[0];
507+
@SuppressWarnings("unchecked")
508+
var selected = (Constructor<T>) only;
509+
return selected;
510+
}
511+
512+
throw new ConstructorNotFoundException(
513+
"No usable constructor on class " + cls.getName()
514+
+ ". Annotate a constructor with MaxMindDbConstructor, "
515+
+ "provide a record canonical constructor, or a single public constructor.");
471516
}
472517

473-
private static <T> String getParameterName(
474-
Class<T> cls,
475-
int index,
476-
Annotation[] annotations
477-
) throws ParameterNotFoundException {
518+
private static String getParameterName(Annotation[] annotations) {
478519
for (var annotation : annotations) {
479520
if (!annotation.annotationType().equals(MaxMindDbParameter.class)) {
480521
continue;
481522
}
482523
var paramAnnotation = (MaxMindDbParameter) annotation;
483524
return paramAnnotation.name();
484525
}
485-
throw new ParameterNotFoundException(
486-
"Constructor parameter " + index + " on class " + cls.getName()
487-
+ " is not annotated with MaxMindDbParameter.");
526+
return null;
488527
}
489528

490529
private long nextValueOffset(long offset, int numberToSkip)

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

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,9 @@ public void testDecodingTypesFile(int chunkSize) throws IOException {
506506
this.testDecodingTypesIntoModelObject(this.testReader, true);
507507
this.testDecodingTypesIntoModelObjectBoxed(this.testReader, true);
508508
this.testDecodingTypesIntoModelWithList(this.testReader);
509+
this.testRecordImplicitConstructor(this.testReader);
510+
this.testSingleConstructorWithoutAnnotation(this.testReader);
511+
this.testPojoImplicitParameters(this.testReader);
509512
}
510513

511514
@ParameterizedTest
@@ -516,6 +519,9 @@ public void testDecodingTypesStream(int chunkSize) throws IOException {
516519
this.testDecodingTypesIntoModelObject(this.testReader, true);
517520
this.testDecodingTypesIntoModelObjectBoxed(this.testReader, true);
518521
this.testDecodingTypesIntoModelWithList(this.testReader);
522+
this.testRecordImplicitConstructor(this.testReader);
523+
this.testSingleConstructorWithoutAnnotation(this.testReader);
524+
this.testPojoImplicitParameters(this.testReader);
519525
}
520526

521527
@ParameterizedTest
@@ -831,6 +837,65 @@ public TestModelList(
831837
}
832838
}
833839

840+
// Record-based decoding without annotations
841+
record MapXRecord(List<Long> arrayX) {}
842+
record MapRecord(MapXRecord mapX) {}
843+
record TestRecordImplicit(MapRecord map) {}
844+
845+
private void testRecordImplicitConstructor(Reader reader) throws IOException {
846+
var model = reader.get(InetAddress.getByName("::1.1.1.0"), TestRecordImplicit.class);
847+
assertEquals(List.of(7L, 8L, 9L), model.map().mapX().arrayX());
848+
}
849+
850+
// Single-constructor classes without @MaxMindDbConstructor
851+
static class MapXPojo {
852+
List<Long> arrayX;
853+
String utf8StringX;
854+
855+
public MapXPojo(
856+
@MaxMindDbParameter(name = "arrayX") List<Long> arrayX,
857+
@MaxMindDbParameter(name = "utf8_stringX") String utf8StringX
858+
) {
859+
this.arrayX = arrayX;
860+
this.utf8StringX = utf8StringX;
861+
}
862+
}
863+
864+
static class MapContainerPojo {
865+
MapXPojo mapX;
866+
867+
public MapContainerPojo(@MaxMindDbParameter(name = "mapX") MapXPojo mapX) {
868+
this.mapX = mapX;
869+
}
870+
}
871+
872+
static class TopLevelPojo {
873+
MapContainerPojo map;
874+
875+
public TopLevelPojo(@MaxMindDbParameter(name = "map") MapContainerPojo map) {
876+
this.map = map;
877+
}
878+
}
879+
880+
private void testSingleConstructorWithoutAnnotation(Reader reader) throws IOException {
881+
var pojo = reader.get(InetAddress.getByName("::1.1.1.0"), TopLevelPojo.class);
882+
assertEquals(List.of(7L, 8L, 9L), pojo.map.mapX.arrayX);
883+
}
884+
885+
// Unannotated parameters on non-record types using Java parameter names
886+
static class TestPojoImplicit {
887+
MapContainerPojo map;
888+
889+
public TestPojoImplicit(MapContainerPojo map) {
890+
this.map = map;
891+
}
892+
}
893+
894+
private void testPojoImplicitParameters(Reader reader) throws IOException {
895+
var model = reader.get(InetAddress.getByName("::1.1.1.0"), TestPojoImplicit.class);
896+
assertEquals(List.of(7L, 8L, 9L), model.map.mapX.arrayX);
897+
}
898+
834899
@ParameterizedTest
835900
@MethodSource("chunkSizes")
836901
public void testZerosFile(int chunkSize) throws IOException {

0 commit comments

Comments
 (0)