Skip to content

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

@KallivdH

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions