Skip to content

Commit 8ffb95a

Browse files
oschwaldclaude
andcommitted
Add @MaxMindDbCreator annotation for custom deserialization
This adds support for marking static factory methods with @MaxMindDbCreator to enable custom deserialization logic, similar to Jackson's @JsonCreator. The decoder now automatically invokes creator methods when decoding values to target types, allowing for custom type conversions such as string-to-enum mappings with non-standard representations. This eliminates the need for redundant constructors that only perform type conversions, as the decoder can now apply conversions automatically via annotated static factory methods. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent d746dad commit 8ffb95a

6 files changed

Lines changed: 311 additions & 2 deletions

File tree

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,38 @@ Lookup context injection
170170
constructor argument. Values are populated for every lookup without being
171171
cached between different IPs.
172172

173+
Custom deserialization
174+
175+
- Use `@MaxMindDbCreator` to mark a static factory method or constructor that
176+
should be used for custom deserialization of a type from a MaxMind DB file.
177+
- This annotation is similar to Jackson's `@JsonCreator` and is useful for
178+
types that need custom deserialization logic, such as enums with non-standard
179+
string representations or types that require special initialization.
180+
- The annotation can be applied to both constructors and static factory methods.
181+
- Example with an enum:
182+
183+
```java
184+
public enum ConnectionType {
185+
DIALUP("Dialup"),
186+
CABLE_DSL("Cable/DSL");
187+
188+
private final String name;
189+
190+
ConnectionType(String name) {
191+
this.name = name;
192+
}
193+
194+
@MaxMindDbCreator
195+
public static ConnectionType fromString(String s) {
196+
return switch (s) {
197+
case "Dialup" -> DIALUP;
198+
case "Cable/DSL" -> CABLE_DSL;
199+
default -> null;
200+
};
201+
}
202+
}
203+
```
204+
173205
You can also use the reader object to iterate over the database.
174206
The `reader.networks()` and `reader.networksWithin()` methods can
175207
be used for this purpose.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.maxmind.db;
2+
3+
import java.lang.reflect.Method;
4+
5+
/**
6+
* Cached creator method information for efficient deserialization.
7+
* A creator method is a static factory method annotated with {@link MaxMindDbCreator}
8+
* that converts a decoded value to the target type.
9+
*
10+
* @param method the static factory method annotated with {@link MaxMindDbCreator}
11+
* @param parameterType the parameter type accepted by the creator method
12+
*/
13+
record CachedCreator(
14+
Method method,
15+
Class<?> parameterType
16+
) {}

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

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import java.lang.annotation.Annotation;
55
import java.lang.reflect.Constructor;
66
import java.lang.reflect.InvocationTargetException;
7+
import java.lang.reflect.Method;
8+
import java.lang.reflect.Modifier;
79
import java.lang.reflect.ParameterizedType;
810
import java.math.BigInteger;
911
import java.net.InetAddress;
@@ -38,6 +40,8 @@ class Decoder {
3840

3941
private final ConcurrentHashMap<Class<?>, CachedConstructor<?>> constructors;
4042

43+
private final ConcurrentHashMap<Class<?>, CachedCreator> creators;
44+
4145
private final InetAddress lookupIp;
4246
private final Network lookupNetwork;
4347

@@ -47,6 +51,7 @@ class Decoder {
4751
buffer,
4852
pointerBase,
4953
new ConcurrentHashMap<>(),
54+
new ConcurrentHashMap<>(),
5055
null,
5156
null
5257
);
@@ -63,6 +68,7 @@ class Decoder {
6368
buffer,
6469
pointerBase,
6570
constructors,
71+
new ConcurrentHashMap<>(),
6672
null,
6773
null
6874
);
@@ -73,13 +79,15 @@ class Decoder {
7379
Buffer buffer,
7480
long pointerBase,
7581
ConcurrentHashMap<Class<?>, CachedConstructor<?>> constructors,
82+
ConcurrentHashMap<Class<?>, CachedCreator> creators,
7683
InetAddress lookupIp,
7784
Network lookupNetwork
7885
) {
7986
this.cache = cache;
8087
this.pointerBase = pointerBase;
8188
this.buffer = buffer;
8289
this.constructors = constructors;
90+
this.creators = creators;
8391
this.lookupIp = lookupIp;
8492
this.lookupNetwork = lookupNetwork;
8593
}
@@ -217,9 +225,11 @@ private <T> Object decodeByType(
217225
}
218226
return this.decodeArray(size, cls, elementClass);
219227
case BOOLEAN:
220-
return Decoder.decodeBoolean(size);
228+
Boolean bool = Decoder.decodeBoolean(size);
229+
return convertValue(bool, cls);
221230
case UTF8_STRING:
222-
return this.decodeString(size);
231+
String str = this.decodeString(size);
232+
return convertValue(str, cls);
223233
case DOUBLE:
224234
return this.decodeDouble(size);
225235
case FLOAT:
@@ -653,6 +663,7 @@ private <T> Object decodeMapIntoObject(int size, Class<T> cls)
653663
private boolean shouldInstantiateFromContext(Class<?> parameterType) {
654664
if (parameterType == null
655665
|| parameterType.isPrimitive()
666+
|| parameterType.isEnum()
656667
|| isSimpleType(parameterType)
657668
|| Map.class.isAssignableFrom(parameterType)
658669
|| List.class.isAssignableFrom(parameterType)) {
@@ -870,6 +881,74 @@ private static void validateInjectionTarget(
870881
}
871882
}
872883

884+
/**
885+
* Converts a decoded value to the target type using a creator method if available.
886+
* If no creator method is found, returns the original value.
887+
*/
888+
private Object convertValue(Object value, Class<?> targetType) {
889+
if (value == null || targetType == null
890+
|| targetType == Object.class
891+
|| targetType.isInstance(value)) {
892+
return value;
893+
}
894+
895+
CachedCreator creator = getCachedCreator(targetType);
896+
if (creator == null) {
897+
return value;
898+
}
899+
900+
if (!creator.parameterType().isInstance(value)) {
901+
return value;
902+
}
903+
904+
try {
905+
return creator.method().invoke(null, value);
906+
} catch (IllegalAccessException | InvocationTargetException e) {
907+
throw new DeserializationException(
908+
"Error invoking creator method " + creator.method().getName()
909+
+ " on class " + targetType.getName(), e);
910+
}
911+
}
912+
913+
private CachedCreator getCachedCreator(Class<?> cls) {
914+
CachedCreator cached = this.creators.get(cls);
915+
if (cached != null) {
916+
return cached;
917+
}
918+
919+
CachedCreator creator = findCreatorMethod(cls);
920+
if (creator != null) {
921+
this.creators.putIfAbsent(cls, creator);
922+
}
923+
return creator;
924+
}
925+
926+
private static CachedCreator findCreatorMethod(Class<?> cls) {
927+
Method[] methods = cls.getDeclaredMethods();
928+
for (Method method : methods) {
929+
if (!method.isAnnotationPresent(MaxMindDbCreator.class)) {
930+
continue;
931+
}
932+
if (!Modifier.isStatic(method.getModifiers())) {
933+
throw new DeserializationException(
934+
"Creator method " + method.getName() + " on class " + cls.getName()
935+
+ " must be static.");
936+
}
937+
if (method.getParameterCount() != 1) {
938+
throw new DeserializationException(
939+
"Creator method " + method.getName() + " on class " + cls.getName()
940+
+ " must have exactly one parameter.");
941+
}
942+
if (!cls.isAssignableFrom(method.getReturnType())) {
943+
throw new DeserializationException(
944+
"Creator method " + method.getName() + " on class " + cls.getName()
945+
+ " must return " + cls.getName() + " or a subtype.");
946+
}
947+
return new CachedCreator(method, method.getParameterTypes()[0]);
948+
}
949+
return null;
950+
}
951+
873952
private static Object parseDefault(String value, Class<?> target) {
874953
try {
875954
if (target.equals(Boolean.TYPE) || target.equals(Boolean.class)) {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.maxmind.db;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
/**
9+
* {@code MaxMindDbCreator} is an annotation that can be used to mark a static factory
10+
* method or constructor that should be used to create an instance of a class from a
11+
* decoded value when decoding a MaxMind DB file.
12+
*
13+
* <p>This is similar to Jackson's {@code @JsonCreator} annotation and is useful for
14+
* types that need custom deserialization logic, such as enums with non-standard
15+
* string representations.</p>
16+
*
17+
* <p>Example usage:</p>
18+
* <pre>
19+
* public enum ConnectionType {
20+
* DIALUP("Dialup"),
21+
* CABLE_DSL("Cable/DSL");
22+
*
23+
* private final String name;
24+
*
25+
* ConnectionType(String name) {
26+
* this.name = name;
27+
* }
28+
*
29+
* {@literal @}MaxMindDbCreator
30+
* public static ConnectionType fromString(String s) {
31+
* return switch (s) {
32+
* case "Dialup" -&gt; DIALUP;
33+
* case "Cable/DSL" -&gt; CABLE_DSL;
34+
* default -&gt; null;
35+
* };
36+
* }
37+
* }
38+
* </pre>
39+
*/
40+
@Retention(RetentionPolicy.RUNTIME)
41+
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
42+
public @interface MaxMindDbCreator {
43+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public final class Reader implements Closeable {
2828
private final AtomicReference<BufferHolder> bufferHolderReference;
2929
private final NodeCache cache;
3030
private final ConcurrentHashMap<Class<?>, CachedConstructor<?>> constructors;
31+
private final ConcurrentHashMap<Class<?>, CachedCreator> creators;
3132

3233
/**
3334
* The file mode to use when opening a MaxMind DB.
@@ -166,6 +167,7 @@ private Reader(BufferHolder bufferHolder, String name, NodeCache cache) throws I
166167
this.ipV4Start = this.findIpV4StartNode(buffer);
167168

168169
this.constructors = new ConcurrentHashMap<>();
170+
this.creators = new ConcurrentHashMap<>();
169171
}
170172

171173
/**
@@ -443,6 +445,7 @@ <T> T resolveDataPointer(
443445
buffer,
444446
this.searchTreeSize + DATA_SECTION_SEPARATOR_SIZE,
445447
this.constructors,
448+
this.creators,
446449
lookupIp,
447450
network
448451
);

0 commit comments

Comments
 (0)