> ## Documentation Index
> Fetch the complete documentation index at: https://lab.pollack.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# JSON Mappers

> Why acp-core has no JSON implementation, the Jackson 2 and 3 modules, and how one gets picked

## `acp-core` has no databind

Since 0.18.0, `acp-core` depends on `jackson-annotations` only, not on `jackson-databind` or
`jackson-core`. The JSON implementation is a separate SPI (`AcpJsonMapper`, chosen at runtime through
`AcpJsonMapperSupplier`, a `ServiceLoader` provider), so applications that need a specific Jackson
version, or a different library entirely, aren't forced into whatever `acp-core` happened to pin.

```java theme={null}
public interface AcpJsonMapper {
    static AcpJsonMapper createDefault();
    <T> T readValue(String json, TypeRef<T> type);
    <T> T convertValue(Object value, TypeRef<T> type);
    String writeValueAsString(Object value);
}
```

If you depend on `acp-core` alone with no JSON module, `AcpJsonMapper.createDefault()` fails with "No
AcpJsonMapperSupplier found on the classpath". `acp-agent-support`, `acp-test`,
`acp-streamable-http-jetty`, and the acp-autoconfig starter all already bring `acp-json-jackson2`
transitively, so most applications never need to think about this; a library consuming `acp-core`
directly does.

## Two modules, one wire format

| Module | Mapper | Priority |
| - | - | - |
| `acp-json-jackson2` | `JacksonAcpJsonMapper` (Jackson 2) | `-200` |
| `acp-json-jackson3` | `Jackson3AcpJsonMapper` (Jackson 3, 3.1.5+, the version Spring Boot 4.1 manages) | `-100` |

Both write identical bytes on the wire; the choice is purely about which Jackson major version your
application already carries. With both modules on the classpath, Jackson 3 wins (the higher
priority); an application's own `AcpJsonMapperSupplier` wins over either, and the system property
`acp.json.mapper.supplier` names one explicitly when more than one choice needs to be pinned.

```java theme={null}
// Starting from a specific, pre-configured ObjectMapper instead of the module's default
var mapper = new JacksonAcpJsonMapper(myObjectMapper);
```

`JacksonAcpJsonMapper.defaultObjectMapper()` / `Jackson3AcpJsonMapper.defaultJsonMapper()` return the
module's own lenient configuration; building a `JacksonAcpJsonMapper` from a bare `new ObjectMapper()`
instead is strict about unknown fields, which can reject forward-compatible messages a newer peer
sends. Start from the module's default and customize it, rather than a bare mapper, unless strictness
is specifically what you want.

## A supported Jackson floor, checked at startup

Frameworks manage their own Jackson version, and earlier SDK versions stated no floor: an older
Jackson than the SDK's tests actually exercise (Vert.x 4.5's jackson-core 2.16.1 under
jackson-databind 2.22.3, for example) could fail much later with a confusing `NoSuchMethodError` while
reading a message, long after the mismatch was introduced. Each mapper supplier and mapper constructor
now checks the jackson-core and jackson-databind actually on the classpath and fails fast with an
`IllegalStateException` naming the version found and the version required:

| Module | Floor | Why |
| - | - | - |
| `acp-json-jackson2` | Jackson 2.18.1 | 2.18.0 drops unknown values of the SDK's open enumerations; 2.17 drops the unknown fields it keeps for forward compatibility |
| `acp-json-jackson3` | Jackson 3.0.0 | The first Jackson 3 release |

Quarkus 3.40 (Jackson 2.21.7) and Spring Boot 4.1 (Jackson 3.1.7) are both comfortably within these
floors. Migration is needed only for an application pinning something older; align the Jackson
artifacts on one version at or above the floor, for example by importing `jackson-bom`.

## Open values and unions still deserialize cleanly

The JSON layer is also what makes [forward compatibility](/docs/acp-java-sdk/forward-compatibility)
actually work: an unrecognized union variant reads as its `Unknown*` record rather than failing the
whole message, and an unrecognized value of an open type (`StopReason`, `ToolCallStatus`, and others)
reads as that same record type holding a string its constants don't list. Neither module treats a
response missing an optional field as an error either: `AcpSchema`'s `DefaultOnNull` marker interface
lets a response read as empty when the peer answers `"result": null` (legal JSON-RPC, and what the
Python SDK sends for a handler returning `None`) or omits the result outright.

## `TypeRef`

Generic type information Java erases at runtime, for reading or converting into a parameterized type:

```java theme={null}
WordCountResult count = client.sendExtRequest(METHOD, params, new TypeRef<WordCountResult>() {});
```

Used throughout the extension-method API (see [Extension Methods](/docs/acp-java-sdk/extensions)) and
anywhere else the SDK needs a caller-supplied type that isn't just `SomeClass.class`.

## Related

<CardGroup cols={2}>
  <Card title="Forward Compatibility" icon="shield-check" href="/docs/acp-java-sdk/forward-compatibility">
    What a newer peer's message looks like once it's deserialized
  </Card>

  <Card title="Installation" icon="download" href="/docs/acp-java-sdk/reference/java#installation">
    Which modules to add, and which transports already bring one
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.