Skip to content

Serialization

Introduction

Koog uses a thin, library-agnostic serialization layer to convert tool arguments and results to and from JSON. This layer sits between the agent runtime and the underlying serialization library, so you can swap the library without changing any tool or agent code.

Beyond tools, the serialization layer is also used by agent features such as Persistence to serialize and deserialize node inputs and outputs.

By default, Koog uses KotlinxSerializer (backed by kotlinx-serialization). On the JVM you can also switch to JacksonSerializer (backed by jackson-databind).

The JSONSerializer interface

JSONSerializer is the core abstraction that lives in serialization-core. The interface has four primary methods (encode/decode to both strings and JSONElement), plus two convenience methods for converting between JSONElement and strings:

  • encodeToString / decodeFromString — serialize a typed value to/from a JSON string.
  • encodeToJSONElement / decodeFromJSONElement — serialize a typed value to/from a JSONElement tree.
  • encodeJSONElementToString / decodeJSONElementFromString — convert between JSONElement and its string form.

The following example shows all key operations:

@Serializable
data class User(val name: String, val age: Int)

val serializer: JSONSerializer = KotlinxSerializer()

// Encode a data class to a JSON string
val json: String = serializer.encodeToString(User("Alice", 30), typeToken<User>())

// Decode a JSON string back to a data class
val user: User = serializer.decodeFromString(json, typeToken<User>())

// Encode to a JSONElement tree
val element: JSONElement = serializer.encodeToJSONElement(user, typeToken<User>())

// Decode from a JSONElement tree
val userFromElement: User = serializer.decodeFromJSONElement(element, typeToken<User>())

// Convert between JSONElement and a raw JSON string
val jsonString = """{"key": "value"}"""
val jsonElement: JSONElement = serializer.decodeJSONElementFromString(jsonString)
val backToString: String = serializer.encodeJSONElementToString(jsonElement)

Type tokens

TypeToken is how Koog passes type information at runtime.

Kotlin

data class MyClass(val value: String)

fun typeTokenExamples() {
    // Inline reified — preferred in Kotlin
    val tokenReified = typeToken<MyClass>()

    // From a KClass (when no reified type parameter is available)
    val tokenKClass = typeToken(MyClass::class)

    // Generic type — preserves type arguments at runtime
    val tokenGeneric = typeToken<List<String>>()
}

Java

// Simple class
TypeToken token = TypeToken.of(MyClass.class);

// Generic type — use TypeCapture to preserve type arguments
TypeToken token = TypeToken.of(new TypeCapture<List<String>>() {});

JSONElement — library-agnostic JSON tree

JSONElement is a neutral intermediate representation for JSON data. It exists so that serializers, tools, and agent internals do not depend on specific JSON types from a particular library.

Hierarchy

JSONElement
├── JSONObject   – key-value pairs  (entries: Map<String, JSONElement>)
├── JSONArray    – ordered list      (elements: List<JSONElement>)
└── JSONPrimitive
    ├── JSONLiteral  – string, number, or boolean
    └── JSONNull     – JSON null singleton

Conversion to and from library types

Each serialization integration provides extension functions that let you convert between JSONElement and the library's own dynamic JSON type. This is useful when you already have a JsonElement or JsonNode and want to pass it to a Koog (or vice versa), without going through a full encode/decode cycle.

Building and reading elements

val obj = JSONObject(
    mapOf(
        "name" to JSONPrimitive("Alice"),
        "age" to JSONPrimitive(30),
        "active" to JSONPrimitive(true),
    )
)

val arr = JSONArray(listOf(JSONPrimitive(1), JSONPrimitive(2), JSONPrimitive(3)))

// Reading values from an object
val nameContent: String = (obj.entries["name"] as JSONPrimitive).content  // "Alice"
val age: Int? = (obj.entries["age"] as JSONPrimitive).intOrNull // 30

Supported serializers

KotlinxSerializer (default)

  • Module: ai.koog:serialization-core (included transitively with ai.koog:agents-core)
  • Backed by: kotlinx-serialization
  • JSONElement mappers: JsonElement.toKoogJSONElement() / JSONElement.toKotlinxJsonElement() (and per-subtype variants)
// Default instance — uses Json.Default
val defaultSerializer = KotlinxSerializer()

// Custom Json configuration
val customSerializer = KotlinxSerializer(
    json = Json {
        ignoreUnknownKeys = true
        prettyPrint = true
    }
)

JacksonSerializer (JVM only)

  • Module: ai.koog:serialization-jackson (separate dependency)
  • Backed by: jackson-databind
  • JSONElement mappers: JsonNode.toKoogJSONElement() / JSONElement.toJacksonJsonNode() (and per-subtype variants)

Add the dependency to your build.gradle.kts:

dependencies {
    implementation("ai.koog:serialization-jackson:<version>")
}

Then create the serializer:

// Default instance — uses a fresh ObjectMapper with JSONElementModule pre-registered
val defaultSerializer = JacksonSerializer()

// Custom ObjectMapper configuration
val customSerializer = JacksonSerializer(
    objectMapper = ObjectMapper().apply {
        configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
    }
)

Note

JacksonSerializer automatically registers JSONElementModule on the ObjectMapper it uses for proper serialization/deserialization of the JSONElement types.

Configuring the serializer in AIAgentConfig

Pass the serializer parameter when constructing AIAgentConfig. If you omit it, KotlinxSerializer() is used.

val agentConfig = AIAgentConfig(
    prompt = prompt("assistant") {
        system("You are a helpful assistant.")
    },
    model = OpenAIModels.Chat.GPT4o,
    maxAgentIterations = 10,
    serializer = JacksonSerializer()
)

How tools interact with the serializer

The agent runtime calls the following methods on each Tool instance automatically. You do not need to invoke them yourself in normal usage.

  • decodeArgs(rawArgs, serializer) (JSON → TArgs) — deserializes raw JSON arguments from the LLM into the tool's typed args class.
  • encodeArgs(args, serializer) (TArgs → JSON) — serializes typed args back to JSON (used by certain agent features).
  • decodeResult(rawResult, serializer) (JSON → TResult) — deserializes a stored JSON result.
  • encodeResult(result, serializer) (TResult → JSON) — serializes the tool's result to JSON.
  • encodeResultToString(result, serializer) (TResult → String) — serializes the tool's result to a string sent to the LLM. By default, delegates to encodeResult. Can be overridden to customize the result format for the LLM.

These methods are open on Tool, so you can override them if you need custom serialization behavior for a specific tool.

How features use the serializer

The serialization layer is not limited to tools — certain agent features rely on it as well.

For example, Persistence uses JSONSerializer configured in AIAgentConfig to serialize and deserialize node inputs and outputs when creating checkpoints and restoring agent state. This means any type that flows through a persisted node must be serializable by the configured JSONSerializer.

See Agent Persistence for details on checkpoint creation and restoration.