Skip to content

Capability records silently drop unknown fields; strict ObjectMapper has no effect #10

Description

@KallivdH

Problem

AcpSchema.AgentCapabilities and sibling capability records carry class-level @JsonIgnoreProperties(ignoreUnknown = true). Any field an agent emits that the SDK's record doesn't declare is silently discarded: no log, no exception, no surfacing via _meta. Consumers cannot detect spec drift after an agent upgrade.

A strict ObjectMapper does not help. From Jackson's javadoc on @JsonIgnoreProperties:

Properties to ignore are defined as the union of values configured through this annotation and any per-class or global configuration.

So the merged ignore set is always a union. Mapper-level FAIL_ON_UNKNOWN_PROPERTIES = true cannot override an annotation-level ignoreUnknown = true.

Verified against acp-core 0.11.0 and 0.12.0 (annotation still present on AgentCapabilities, ClientCapabilities, FileSystemCapability, PromptCapabilities, McpCapabilities, and others).

Reproducer

Stub agent returns AgentCapabilities with a non-spec field fabricatedFeature: true. Client uses default JacksonAcpJsonMapper:

AcpSchema.InitializeResponse resp = client.initialize(
    new AcpSchema.InitializeRequest(1, new AcpSchema.ClientCapabilities()));

// Known field round-trips fine
assertEquals(Boolean.FALSE, resp.agentCapabilities().loadSession());
// Unknown field is gone, not in _meta, not in logs
assertNull(resp.agentCapabilities().meta());

Same result when passing a custom strict mapper:

ObjectMapper strict = new ObjectMapper()
    .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
new JacksonAcpJsonMapper(strict);  // no-op, record-level annotation wins

Contrast with #4: unknown polymorphic subtypes on SessionUpdate fail loud (Error handling notification: Could not resolve type id ...). Unknown fields on known records fail silent. Same root cause (agent newer than SDK), opposite observability.

Proposal

Preferred, one-line change: remove @JsonIgnoreProperties(ignoreUnknown = true) from the capability records. Default Jackson behavior still tolerates unknown fields unless the consumer opts into strict mode via DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES. This gives consumers the choice without changing the default outward behavior.

Alternative: route unknown fields into a typed extension map. A separate Map<String, Object> unknownFields is cleaner than overloading _meta, which already has spec-defined semantics as a metadata extension point. Conflating "agent sent metadata" with "agent sent a field we don't know" makes drift detection harder, not easier. This needs a custom deserializer or compact-constructor merge on each record (records do not accept a method-level @JsonAnySetter on the canonical constructor trivially), so it is a bigger patch.

Happy to open a PR for option 1.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions