A comprehensive visual reference to DAP—the JSON-based protocol that standardizes communication between development tools and debuggers. With Kotlin & Java examples.
How DAP eliminates the M x N debugger integration problem
The Debug Adapter Protocol (DAP) is a JSON-based protocol created by Microsoft that standardizes how development tools (IDEs and editors) communicate with debuggers.
Before DAP, every IDE needed a custom integration for every debugger—creating an M × N problem. With M editors and N debugger backends, you needed M×N integrations. DAP reduces this to M + N: each editor implements one DAP client, and each debugger provides one DAP adapter.
DAP uses a request-response pattern with asynchronous events. Messages are JSON objects transported over stdin/stdout or TCP sockets, framed with HTTP-style Content-Length headers.
Content-Length: 119\r\n
\r\n
{
"seq": 1,
"type": "request",
"command": "initialize",
"arguments": {
"clientID": "vscode",
"adapterID": "kotlin"
}
}Each message is preceded by a header with Content-Length followed by CRLFCRLF, then the JSON payload.
Client, Adapter, Debuggee: the three-actor model
The IDE or editor (VS Code, Neovim, IntelliJ). Provides debug UI for breakpoints, stepping, variable inspection. Sends DAP requests.
The intermediary that translates DAP into native debug APIs. For Kotlin/JVM: the kotlin-debug-adapter or java-debug server using JDWP.
The target program being debugged. Runs on the JVM with the debug agent enabled (-agentlib:jdwp). Controlled via the adapter.
Three message types: Request, Response, and Event
From initialization to disconnect — the complete session lifecycle
Client and adapter negotiate capabilities
Set breakpoints, exception filters, function breakpoints
Launch or attach, then step/continue through code
Disconnect, clean up debuggee process
All major DAP request types with JSON examples
Asks the adapter to launch the debuggee. For Kotlin/JVM, this typically launches the JVM with debug instrumentation.
1package com.example.debug23class="kt-keyword">fun factorial(n: Int): Long {4 class="kt-keyword">if (n <= class="kt-number">1) class="kt-keyword">return class="kt-number">1L5 class="kt-keyword">return n * factorial(n - class="kt-number">1)6}78class="kt-keyword">fun main() {9 class="kt-keyword">val number = class="kt-number">510 println(class="kt-string">"Computing factorial of $number")11 class="kt-keyword">val result = factorial(number)12 println(class="kt-string">"factorial($number) = $result")13}
{
"seq": 5,
"type": "request",
"command": "launch",
"arguments": {
"type": "kotlin",
"name": "Launch Factorial",
"request": "launch",
"mainClass": "com.example.debug.MainKt",
"projectRoot": "/home/dev/factorial-project",
"classPaths": ["/home/dev/factorial-project/build/classes/kotlin/main"],
"vmArguments": "-Xmx256m",
"noDebug": false
}
}Attaches to an already-running debuggee. The target JVM must have the JDWP agent listening.
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005 -jar app.jar{
"seq": 5,
"type": "request",
"command": "attach",
"arguments": {
"type": "kotlin",
"hostName": "localhost",
"port": 5005,
"timeout": 10000
}
}Sets all breakpoints for a given source file. Replaces all previous breakpoints in that file.
{
"seq": 4,
"type": "request",
"command": "setFunctionBreakpoints",
"arguments": {
"breakpoints": [
{ "name": "factorial" },
{ "name": "main", "condition": "args.size > 0" }
]
}
}Configures which exceptions should cause the debugger to break. Filter IDs come from theexceptionBreakpointFilters capability.
{
"seq": 4,
"type": "request",
"command": "setExceptionBreakpoints",
"arguments": {
"filters": ["uncaught"],
"filterOptions": [
{ "filterId": "caught", "condition": "java.lang.ArithmeticException" }
]
}
}Sent after all breakpoints and configuration. Tells the adapter to start or resume the debuggee.
{ "seq": 6, "type": "request", "command": "configurationDone" }{
"seq": 10,
"type": "response",
"request_seq": 9,
"command": "threads",
"success": true,
"body": {
"threads": [
{ "id": 1, "name": "main" },
{ "id": 2, "name": "Reference Handler" },
{ "id": 3, "name": "Finalizer" },
{ "id": 4, "name": "Signal Dispatcher" }
]
}
}{
"seq": 14,
"type": "response",
"request_seq": 13,
"command": "scopes",
"success": true,
"body": {
"scopes": [
{
"name": "Locals",
"presentationHint": "locals",
"variablesReference": 1001,
"namedVariables": 2,
"expensive": false
}
]
}
}Retrieves all child variables for a given variablesReference.
{
"seq": 16,
"type": "response",
"request_seq": 15,
"command": "variables",
"success": true,
"body": {
"variables": [
{ "name": "n", "value": "5", "type": "Int", "variablesReference": 0, "evaluateName": "n" },
{ "name": "this", "value": "MainKt", "type": "com.example.debug.MainKt", "variablesReference": 1002 }
]
}
}Evaluates an expression in the context of a stack frame. Used for debug console, hover tooltips, and watch expressions.
continueResume until next breakpoint or end
nextStep over — execute current line, stop at next
stepInStep into — enter function calls
stepOutStep out — run until current function returns
{
"seq": 25,
"type": "request",
"command": "next",
"arguments": { "threadId": 1, "granularity": "statement" }
}{ "seq": 30, "type": "request", "command": "pause", "arguments": { "threadId": 1 } }{
"seq": 35,
"type": "request",
"command": "setVariable",
"arguments": { "variablesReference": 1001, "name": "n", "value": "10" }
}Retrieves source code for a source reference. Used when source is not available locally (e.g., decompiled bytecode).
{
"seq": 40,
"type": "request",
"command": "source",
"arguments": { "source": { "sourceReference": 1000 }, "sourceReference": 1000 }
}Ends the debug session. terminateDebuggee controls whether the process is also killed.
{
"seq": 50,
"type": "request",
"command": "disconnect",
"arguments": { "restart": false, "terminateDebuggee": true }
}{ "seq": 48, "type": "request", "command": "terminate", "arguments": { "restart": false } }{ "seq": 55, "type": "request", "command": "restart", "arguments": {} }{
"seq": 60,
"type": "request",
"command": "completions",
"arguments": { "frameId": 1, "text": "fact", "column": 5 }
}{
"seq": 60,
"type": "response",
"request_seq": 60,
"command": "completions",
"success": true,
"body": {
"targets": [
{ "label": "factorial", "type": "function" },
{ "label": "factorialResult", "type": "variable" }
]
}
}{
"seq": 45,
"type": "response",
"request_seq": 44,
"command": "exceptionInfo",
"success": true,
"body": {
"exceptionId": "java.lang.StackOverflowError",
"description": "Stack overflow in recursive call",
"breakMode": "always",
"details": {
"message": "Stack overflow in recursive call",
"typeName": "java.lang.StackOverflowError",
"stackTrace": "at com.example.debug.MainKt.factorial(main.kt: 5)\n..."
}
}
}Asynchronous notifications from the adapter to the client
Adapter is ready for configuration requests
Execution stopped (breakpoint, step, exception, pause)
Execution has resumed
Debuggee has exited with an exit code
Debugging session is finished
A thread has started or exited
Program output (stdout, stderr, console)
Breakpoint state changed (verified, moved)
Module (JAR, class file) loaded/changed
Debuggee process information
Adapter capabilities have changed
Source file loaded, changed, or removed
{
"seq": 20,
"type": "event",
"event": "output",
"body": {
"category": "stdout",
"output": "Computing factorial of 5\n",
"source": { "name": "main.kt", "path": "/home/dev/project/src/main/kotlin/main.kt" },
"line": 10
}
}{
"seq": 8,
"type": "event",
"event": "thread",
"body": { "reason": "started", "threadId": 1 }
}{ "seq": 50, "type": "event", "event": "exited", "body": { "exitCode": 0 } }{ "seq": 51, "type": "event", "event": "terminated" }{
"seq": 7,
"type": "event",
"event": "process",
"body": {
"name": "com.example.debug.MainKt",
"systemProcessId": 12345,
"isLocalProcess": true,
"startMethod": "launch"
}
}{
"seq": 9,
"type": "event",
"event": "breakpoint",
"body": {
"reason": "changed",
"breakpoint": { "id": 1, "verified": true, "line": 4 }
}
}Step-by-step walkthrough of debugging a Kotlin factorial function
1package com.example.debug23class="kt-keyword">fun factorial(n: Int): Long {4 class="kt-keyword">if (n <= class="kt-number">1) class="kt-keyword">return class="kt-number">1L class=class="kt-string">"kt-comment">// Line class="kt-number">4: base case5 class="kt-keyword">return n * factorial(n - class="kt-number">1) class=class="kt-string">"kt-comment">// Line class="kt-number">5: recursive call6}78class="kt-keyword">fun main() {9 class="kt-keyword">val number = class="kt-number">5 class=class="kt-string">"kt-comment">// Line class="kt-number">910 println(class="kt-string">"Computing factorial of $number") class=class="kt-string">"kt-comment">// Line class="kt-number">1011 class="kt-keyword">val result = factorial(number) class=class="kt-string">"kt-comment">// Line class="kt-number">11: we call factorial here12 println(class="kt-string">"factorial($number) = $result") class=class="kt-string">"kt-comment">// Line class="kt-number">1213}
Follow the complete DAP message exchange. We set a breakpoint at line 4 (insidefactorial), then step through each recursive call.
Client connects and negotiates capabilities with the Kotlin debug adapter.
{
"seq": 1,
"type": "request",
"command": "initialize",
"arguments": {
"clientID": "vscode",
"adapterID": "kotlin",
"linesStartAt1": true,
"columnsStartAt1": true,
"supportsVariableType": true
}
}Adapter reports its capabilities.
{
"seq": 1,
"type": "response",
"request_seq": 1,
"command": "initialize",
"success": true,
"body": {
"supportsConfigurationDoneRequest": true,
"supportsConditionalBreakpoints": true,
"supportsEvaluateForHovers": true,
"supportsSetVariable": true,
"supportsTerminateRequest": true,
"exceptionBreakpointFilters": [
{ "filter": "caught", "label": "Caught Exceptions" },
{ "filter": "uncaught", "label": "Uncaught Exceptions", "default": true }
]
}
}Adapter signals it's ready for configuration.
{ "seq": 2, "type": "event", "event": "initialized" }Set a breakpoint at line 4 inside the factorial function.
{
"seq": 2,
"type": "request",
"command": "setBreakpoints",
"arguments": {
"source": { "name": "main.kt", "path": "/home/dev/project/src/main/kotlin/main.kt" },
"breakpoints": [{ "line": 4 }]
}
}Breakpoint verified at line 4.
{
"seq": 3,
"type": "response",
"request_seq": 2,
"command": "setBreakpoints",
"success": true,
"body": { "breakpoints": [{ "id": 1, "verified": true, "line": 4 }] }
}All configuration sent. Adapter can start the debuggee.
{ "seq": 3, "type": "request", "command": "configurationDone" }Launch the Kotlin program with debugging enabled.
{
"seq": 4,
"type": "request",
"command": "launch",
"arguments": {
"mainClass": "com.example.debug.MainKt",
"projectRoot": "/home/dev/project",
"classPaths": ["/home/dev/project/build/classes/kotlin/main"]
}
}The JVM process has started.
{
"seq": 5,
"type": "event",
"event": "process",
"body": { "name": "com.example.debug.MainKt", "systemProcessId": 28456, "startMethod": "launch" }
}Program prints to stdout before hitting the breakpoint.
{
"seq": 7,
"type": "event",
"event": "output",
"body": { "category": "stdout", "output": "Computing factorial of 5\n" }
}factorial(5) called, execution stops at line 4.
Breakpoint hit at line 4 — first recursive call with n=5
{
"seq": 8,
"type": "event",
"event": "stopped",
"body": { "reason": "breakpoint", "threadId": 1, "allThreadsStopped": true, "hitBreakpointIds": [1] }
}Client requests the call stack.
{
"seq": 10,
"type": "response",
"request_seq": 9,
"command": "stackTrace",
"success": true,
"body": {
"stackFrames": [
{ "id": 1, "name": "factorial", "line": 4, "column": 5,
"source": { "name": "main.kt", "path": "/home/dev/project/src/main/kotlin/main.kt" } },
{ "id": 2, "name": "main", "line": 11, "column": 18,
"source": { "name": "main.kt", "path": "/home/dev/project/src/main/kotlin/main.kt" } }
],
"totalFrames": 2
}
}Client inspects variables in the factorial frame.
{
"body": {
"variables": [
{ "name": "n", "value": "5", "type": "Int", "variablesReference": 0 }
]
}
}Variable state: n = 5 (first call: factorial(5))
Resume execution. factorial(4) will hit the breakpoint again.
{ "seq": 15, "type": "request", "command": "continue", "arguments": { "threadId": 1 } }Second recursive call hits the breakpoint.
Use the debug console to evaluate an expression.
{
"seq": 22,
"type": "request",
"command": "evaluate",
"arguments": { "expression": "n * 2", "frameId": 1, "context": "repl" }
}{
"body": { "result": "8", "type": "Int", "variablesReference": 0 }
}Step into the recursive call to follow factorial(3).
{ "seq": 25, "type": "request", "command": "stepIn", "arguments": { "threadId": 1 } }Now inside factorial(3).
Let the program run to completion.
After continuing through all recursive calls:
Program prints the final result.
{
"seq": 40,
"type": "event",
"event": "output",
"body": { "category": "stdout", "output": "factorial(5) = 120\n" }
}Program exits normally with code 0.
{ "seq": 45, "type": "event", "event": "exited", "body": { "exitCode": 0 } }Debug session is complete.
{ "seq": 46, "type": "event", "event": "terminated" }Client cleanly disconnects.
{
"seq": 50,
"type": "request",
"command": "disconnect",
"arguments": { "restart": false, "terminateDebuggee": false }
}How DAP works with the JVM via JDWP and JDI
When debugging Kotlin/Java, the debug adapter communicates with the JVM through the Java Debug Wire Protocol (JDWP).
{
"version": "0.2.0",
"configurations": [
{
"type": "kotlin",
"request": "launch",
"name": "Launch Factorial",
"mainClass": "com.example.debug.MainKt",
"projectRoot": "${workspaceFolder}",
"classPaths": [
"${workspaceFolder}/build/classes/kotlin/main"
]
},
{
"type": "kotlin",
"request": "attach",
"name": "Attach to JVM",
"hostName": "localhost",
"port": 5005,
"timeout": 10000
}
]
}class=class="kt-string">"kt-comment">// Launch JVM with debug agentclass=class="kt-string">"kt-comment">// suspend=y: wait for debuggerjava -agentlib:jdwp=transport=dt_socket,\server=y,suspend=y,address=*:class="kt-number">5005 \-jar myapp.jar