When does a GraalVM native image need Java serialization hints (serialization-config.json / SerializationHints), and how do you register them in Spring?
answer
- ObjectOutputStream/ObjectInputStream only
- NOT Jackson JSON (that's reflection/binding hints)
- hints.serialization().registerType(...)
- serialization-config.json 'types'
- Register the whole transitive graph
basics
~10 sOnly classes actually put through Java's ObjectInputStream/ObjectOutputStream need serialization hints. GraalVM excludes serialization metadata by default. In Spring, call hints.serialization().registerType(Foo.class) in a RuntimeHintsRegistrar, or list the class in serialization-config.json.
solid answer
~40 sJava built-in serialization (`Serializable` + `ObjectOutputStream`/`ObjectInputStream`) relies on reflective access to fields and generated serialization methods that GraalVM's closed-world build won't include unless told. So any type that is genuinely serialized/deserialized through the JDK mechanism needs a serialization hint. Note this is **Java native serialization only** — JSON via Jackson uses reflection/binding hints (`@RegisterReflectionForBinding`), not serialization hints. You register with GraalVM's `serialization-config.json` (`{"types":[{"name":"com.acme.Foo"}]}`) or, in Spring, `hints.serialization().registerType(TypeReference.of(Foo.class))` inside a `RuntimeHintsRegistrar`, wired via `@ImportRuntimeHints`. It's needed for things like HTTP session objects serialized to a store, RMI, or caches that use JDK serialization. Because Java serialization is comparatively rare in modern apps, this is the least-used hint category.
code
java · 12 linesimport org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.TypeReference;
class SessionSerializationHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader cl) {
// Only for types actually written/read via ObjectOutputStream/ObjectInputStream
hints.serialization().registerType(TypeReference.of("com.acme.CartState"));
hints.serialization().registerType(TypeReference.of("com.acme.LineItem")); // nested field type
}
}go deeper
Aware serialization hints exist for Java-serialized objects.
Can register a type with hints.serialization().registerType and knows it's for ObjectInputStream/OutputStream.
Distinguishes JDK serialization from JSON binding, registers the transitive graph, knows where JDK serialization hides (sessions/caches/RMI).
Advocates eliminating JDK serialization in native apps entirely, understands security rationale for it being off by default.
## What counts as 'serialization' here This is specifically **JDK built-in serialization**: a class implements `java.io.Serializable`, and something writes/reads it with `ObjectOutputStream.writeObject` / `ObjectInputStream.readObject`. The JDK does this via **reflection** on the object's fields plus special methods (`writeObject`, `readObject`, `writeReplace`, `readResolve`) and computes a `serialVersionUID`. All of that reflective machinery must be present in the image. Under GraalVM's closed-world model, serialization metadata is **not** included by default (it's a security-and-size decision — Java serialization is a common vulnerability vector). So a class you deserialize at runtime will fail unless registered. ## This is NOT about JSON/XML A frequent misconception: 'my REST DTOs need serialization hints.' They don't — Jackson/Gson serialize to **JSON via reflection or generated binding**, which needs **reflection hints** (`ReflectionHints`) or Spring's `@RegisterReflectionForBinding` / `BindingReflectionHintsRegistrar`, *not* `SerializationHints`. `SerializationHints` is exclusively for the `java.io` object-stream mechanism. ## Where JDK serialization actually appears - HTTP **session** replication/persistence when sessions are serialized (e.g. some Spring Session backends). - Distributed caches / grids that default to Java serialization. - RMI and some messaging payloads. - `Exception` objects sent across a boundary (they're `Serializable`). ## Registering **GraalVM `serialization-config.json`:** ```json { "types": [ { "name": "com.acme.OrderSnapshot" } ] } ``` **Spring RuntimeHints:** ```java hints.serialization().registerType(TypeReference.of(OrderSnapshot.class)); // or the Class overload hints.serialization().registerType(OrderSnapshot.class); ``` Wired via a `RuntimeHintsRegistrar` + `@ImportRuntimeHints`. ## Gotchas - **Register the whole graph.** Serialization is transitive: fields that are themselves serialized (nested objects, collection element types) each need registration. Miss one and deserialization fails deep in the object graph. - **Superclasses / `serialPersistentFields`** may also need registration. - **Lambdas / proxies** that are serialized have extra requirements — generally avoid serializing them in native. - **Symptom** appears only in the native binary as a serialization exception; JVM tests pass. The tracing agent captures `writeObject`/`readObject` calls and can populate the config. - Prefer **not using JDK serialization** in native apps at all — switch session/cache stores to JSON or a binary codec, eliminating the hint need entirely. ## When to use Register serialization hints only when you truly can't avoid JDK object streams. Otherwise, redesign to JSON/Protobuf and use binding/reflection hints instead.
- A candidate says their Jackson REST DTOs need serialization-config.json. Correct them.No — Jackson uses reflection over getters/fields to produce JSON, so those DTOs need reflection/binding hints (@RegisterReflectionForBinding or ReflectionHints), not SerializationHints. Serialization hints are exclusively for java.io ObjectOutputStream/ObjectInputStream.
- Why register nested field types too, and how do you find omissions?JDK serialization is transitive — every serialized object in the graph is reflected over, so nested/element types each need a hint or deserialization fails deep in the graph. Use the GraalVM tracing agent to capture the full set from a real run.