@RegisterForReflection vs @Serializable in Quarkus with kotlin

Hemanth N V
7 min readMay 22, 2024

Overview of @RegisterForReflection

In Quarkus, @RegisterForReflection is an annotation used to ensure that certain classes, methods, or fields are retained for reflection when the application is compiled into a native executable using GraalVM. This is necessary because GraalVM performs aggressive dead code elimination and may remove elements that are only accessed through reflection, leading to runtime errors. The @RegisterForReflection annotation prevents this by explicitly marking elements that should be preserved.

Detailed Code Example

Here’s your provided Kotlin code with explanations:

import com.fasterxml.jackson.annotation.JsonProperty
import io.quarkus.runtime.annotations.RegisterForReflection

@RegisterForReflection
data class CreateProductInfoRequest(
@field:JsonProperty("code")
val code: String,
@field:JsonProperty("itemRefNum")
val itemRefNum: String,
@field:JsonProperty("site")
val site: String,
@field:JsonProperty("commercialName")
val commercialName: String,
@field:JsonProperty("packagedQuantity")
var packagedQuantity: Double,
@field:JsonProperty("shippedQuantity")
var shippedQuantity: Double,
@field:JsonProperty("state")
var state: String
) {
fun toEntity(): ProductInfo {
return ProductInfo().apply {
this.code = this@CreateProductInfoRequest.code
this.itemRefNum = this@CreateProductInfoRequest.itemRefNum
this.site = this@CreateProductInfoRequest.site
this.commercialName = this@CreateProductInfoRequest.commercialName
this.packagedQuantity = this@CreateProductInfoRequest.packagedQuantity
this.shippedQuantity = this@CreateProductInfoRequest.shippedQuantity
this.state = this@CreateProductInfoRequest.state
}
}
}

Detailed Explanation

  1. Importing Necessary Annotations:
import com.fasterxml.jackson.annotation.JsonProperty
import io.quarkus.runtime.annotations.RegisterForReflection
  • JsonProperty: This annotation from the Jackson library is used to specify the JSON property names that should be mapped to the fields in the data class. This is crucial for JSON serialization and deserialization.
  • RegisterForReflection: This annotation from Quarkus marks the class to be included in the reflection configuration of the native image, ensuring that it is available for reflection at runtime.

2. Applying @RegisterForReflection to the Data Class:

@RegisterForReflection
data class CreateProductInfoRequest(
  • The @RegisterForReflection annotation is applied to the CreateProductInfoRequest data class. This indicates to the Quarkus framework that the class and its members should be preserved for reflection in the native image. This is necessary for JSON serialization/deserialization, which uses reflection to access the fields.

3. Field-Level Annotations:

@field:JsonProperty("code")
val code: String,
  • @field:JsonProperty: This specifies that the code field should be mapped to the code property in the JSON object. Similar annotations are applied to other fields (itemRefNum, site, commercialName, packagedQuantity, shippedQuantity, state), ensuring that they are correctly mapped during JSON processing.

4. Method Implementation:

fun toEntity(): ProductInfo {
return ProductInfo().apply {
this.code = this@CreateProductInfoRequest.code
this.itemRefNum = this@CreateProductInfoRequest.itemRefNum
this.site = this@CreateProductInfoRequest.site
this.commercialName = this@CreateProductInfoRequest.commercialName
this.packagedQuantity = this@CreateProductInfoRequest.packagedQuantity
this.shippedQuantity = this@CreateProductInfoRequest.shippedQuantity
this.state = this@CreateProductInfoRequest.state
}
}
  • toEntity Method: This method converts an instance of CreateProductInfoRequest into an instance of ProductInfo. The Kotlin apply function is used to set the properties of ProductInfo using the values from CreateProductInfoRequest.

Why Use @RegisterForReflection?

  • GraalVM Native Image Compatibility: GraalVM’s native image compilation process removes unused code, including classes, methods, and fields that are only accessed via reflection. This can break functionalities like JSON serialization/deserialization, which rely on reflection. By using @RegisterForReflection, you ensure that the necessary metadata for reflection is retained in the native image.
  • Avoiding Runtime Errors: Missing reflection metadata can cause runtime errors in native images, particularly in frameworks that depend heavily on reflection (like Jackson for JSON processing). The @RegisterForReflection annotation helps avoid these errors by explicitly marking the classes and members that need to be available for reflection.
  • Maintaining Application Functionality: Ensuring that classes used in serialization/deserialization or other reflection-based operations are preserved in the native image maintains the correct functionality of your application.

Considerations

  • Selective Use: Only use @RegisterForReflection for classes and members that are accessed via reflection. Overusing this annotation can lead to larger native images and potentially impact performance.
  • Configuration Maintenance: As your application evolves, you need to keep the reflection configuration up-to-date to include new classes or members accessed via reflection.

By understanding and applying the @RegisterForReflection annotation correctly, you can ensure that your Quarkus applications work seamlessly when compiled into native executables, preserving necessary reflection metadata and avoiding runtime issues.

Overview of @Serializable

The @Serializable annotation is part of Kotlin's serialization library (kotlinx.serialization). This annotation is used to mark a class as serializable, meaning that instances of the class can be converted to and from formats like JSON, ProtoBuf, and others. This is particularly useful for data transfer between different parts of an application or between different systems.

Example Code

Here’s a new Kotlin example with the @Serializable annotation:

import kotlinx.serialization.Serializable

@Serializable
data class User(
val id: Int,
val name: String,
val email: String,
val isActive: Boolean
)

Detailed Explanation

  1. Importing the @Serializable Annotation:
import kotlinx.serialization.Serializable
  • This import statement includes the @Serializable annotation from the Kotlin serialization library (kotlinx.serialization). This library provides tools for converting Kotlin objects to various formats like JSON.

2. Applying @Serializable to the Data Class:

@Serializable
data class User(
  • The @Serializable annotation is applied to the User data class. This marks the class as serializable, meaning that instances of this class can be converted to and from JSON (or other formats).

3. Fields in the Data Class:

val id: Int,
val name: String,
val email: String,
val isActive: Boolean
  • The fields in the User class represent the data that can be serialized. Fields can be of various types:
  • Primitive types: Int, Boolean
  • String type: String

Serialization and Deserialization

Serialization

Serialization is the process of converting an object into a format that can be easily stored or transmitted. For example, converting a User instance to a JSON string.

Example:

import kotlinx.serialization.json.Json

val user = User(
id = 1,
name = "John Doe",
email = "john.doe@example.com",
isActive = true
)
val jsonString = Json.encodeToString(User.serializer(), user)
println(jsonString

Output:

{
"id": 1,
"name": "John Doe",
"email": "john.doe@example.com",
"isActive": true
}

In this example:

  • Json.encodeToString is used to serialize the user object to a JSON string.
  • The User.serializer() method is used to obtain the serializer for the User class.

Deserialization

Deserialization is the process of converting a format (like JSON) back into an object.

Example:

val jsonString = """
{
"id": 1,
"name": "John Doe",
"email": "john.doe@example.com",
"isActive": true
}
"""

val user = Json.decodeFromString(User.serializer(), jsonString)
println(user)

Output:

User(id=1, name=John Doe, email=john.doe@example.com, isActive=true)

In this example:

  • Json.decodeFromString is used to deserialize the JSON string back into a User object.
  • The User.serializer() method is used to obtain the serializer for the User class.

Integration with Quarkus

Quarkus supports Kotlin serialization through extensions and integrates seamlessly with RESTful web services, making it easy to use data classes annotated with @Serializable in REST endpoints.

Example REST Endpoint:

import javax.ws.rs.*
import javax.ws.rs.core.MediaType
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToString
import kotlinx.serialization.decodeFromString

@Path("/users")
class UserResource {
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
fun createUser(userJson: String): String {
val user = Json.decodeFromString<User>(userJson)
// Process the user object (e.g., save to database)
return Json.encodeToString(user)
}
@GET
@Path("/{id}")
@Produces(MediaType.APPLICATION_JSON)
fun getUser(@PathParam("id") id: Int): String {
// Retrieve the user object based on the id (e.g., from database)
val user = User(id, "John Doe", "john.doe@example.com", true)
return Json.encodeToString(user)
}
}

In this example:

  • The createUser method consumes a JSON string representing a User object, deserializes it, processes it, and then returns the serialized JSON string.
  • The getUser method retrieves a User object based on a provided ID and returns it as a JSON string.

Benefits of Using @Serializable

  1. Type-Safety: Ensures that the data being serialized and deserialized matches the structure defined in the data class.
  2. Ease of Use: Simplifies the process of converting objects to and from various formats, such as JSON.
  3. Integration: Works seamlessly with Quarkus and other frameworks for handling data transfer and persistence.
  4. Flexibility: Supports various formats (JSON, ProtoBuf, etc.) through different serialization modules.

Detailed Comparison

Purpose

@RegisterForReflection:

  • Primarily used to register classes that require reflection at runtime.
  • Helps Quarkus optimize the application by reducing unnecessary reflection use.

@Serializable:

  • Used to mark classes for serialization and deserialization.
  • Facilitates converting objects to and from formats like JSON.

Use Case

@RegisterForReflection:

  • Needed when your application or libraries use reflection, and you want to explicitly specify which classes need to be registered for reflection.
  • Commonly used in native image generation (GraalVM) to ensure that reflection metadata is available at runtime.

@Serializable:

  • Needed when you need to serialize and deserialize objects to/from JSON or other formats.
  • Useful for REST APIs, data persistence, and inter-service communication.

Example in Kotlin

Using @RegisterForReflection

import io.quarkus.runtime.annotations.RegisterForReflection

@RegisterForReflection
data class User(
val id: Int,
val name: String,
val email: String
)
// Example usage in a Quarkus application
fun reflectUserClass() {
val clazz = Class.forName("com.example.User")
val constructor = clazz.getConstructor(Int::class.java, String::class.java, String::class.java)
val user = constructor.newInstance(1, "John Doe", "john.doe@example.com")
println(user)
}

Using @Serializable

import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

@Serializable
data class User(
val id: Int,
val name: String,
val email: String
)
// Serialization example
fun serializeUser() {
val user = User(1, "John Doe", "john.doe@example.com")
val jsonString = Json.encodeToString(User.serializer(), user)
println(jsonString)
}
// Deserialization example
fun deserializeUser(jsonString: String) {
val user = Json.decodeFromString(User.serializer(), jsonString)
println(user)
}

Integration with Quarkus

@RegisterForReflection:

  • Directly integrated with Quarkus to optimize reflection usage.
  • Useful in native image scenarios with GraalVM, where reflection metadata needs to be retained.

@Serializable:

  • Requires kotlinx.serialization library.
  • Easily integrates with Quarkus REST endpoints for JSON (or other formats) data handling.
  • Needs additional setup for serialization configurations.

Performance

  • @RegisterForReflection:
  • Helps improve performance by reducing unnecessary reflection.
  • Useful for optimizing startup times and memory usage, especially in native images.
  • @Serializable:
  • Provides efficient serialization and deserialization mechanisms.
  • Optimized for performance in converting data to and from JSON (or other formats).

Summary

  • @RegisterForReflection is used to register classes for reflection in Quarkus, optimizing reflection usage and improving performance, especially in native images.
  • @Serializable is used to mark classes for serialization and deserialization, facilitating data conversion to and from formats like JSON.

Both annotations serve different purposes and can be used together in a Quarkus application to handle reflection and serialization needs effectively.

--

--

Hemanth N V

Staff Software Engineer, (Technologies Java, Kotlin, Android, AWS)