Tutorial

A2A 1.0 Agent Card JSON in practice: how to validate capability claims, skill shape, and cross-agent payloads

When validation fails, name the layer first: it parses, it passed the generated schema, or it matched the spec’s required table.

The last post, the discovery walk, finished well-known, catalog, then the Card. How to write the fields is the earlier Agent Card field tutorial. Today we do not refill fields, and we do not walk hops again. You already hold a card, or you just GET one. The question is: how do you assert it is legal, and will the Task you send be treated as garbage? The text lives in the A2A 1.0 spec §4.4 and §3.3.4. The site’s a2a.json says it itself: a non-normative JSON Schema bundle extracted from proto. The 0.3 → 1.0 move is on what’s new in v1.0.

Green is not a pass

Drop the generated bundle into Ajv, point at the root, watch it go green — that is the most common false pass in review. The root is not a Card. AgentCard, AgentSkill, Task, and Message live under $defs. Validating the bundle root validates nothing. The right pointer is #/$defs/AgentCard. Traffic uses #/$defs/Task or #/$defs/Message. Do not wrap a Task in the Card schema.

Even with the right pointer, the generated bundle often omits required. The September 2026 site copy of AgentCard and AgentSkill sets additionalProperties: false but does not list the spec table’s required keys. An empty name, a skill with no tags, can still go green. The spec table is the source of required: name, description, version, supportedInterfaces, capabilities, defaultInputModes, defaultOutputModes, skills. Each skill still needs id / name / description / tags.

So you validate at least twice. First pass: it parses, and the generated AgentCard does not explode on extra keys. Second pass: the spec table asserts non-empty. First pass only catches a leftover 0.3 top-level url via additionalProperties: false; a skill without tags slips through. Second pass only, without the generated bundle, misses a stray inputSchema. You need both.

This layer What it catches What it misses
JSON.parseBroken syntax, truncation, trailing commasWhether the shape is right
Generated #/$defs/AgentCard0.3 top-level url, inputSchema on a skillWhether spec-required keys were written
Spec §4.4 required tableEmpty name, missing tags, empty skillsWhether flags match the operation you are about to send

Four layers, one class of error each

Layer three is capability flags. Spec §3.3.4 is blunt: streaming false and you still subscribe, the agent MUST return UnsupportedOperationError. pushNotifications false and you still configure a webhook, PushNotificationNotSupportedError. extendedAgentCard false and you still fetch the extended card, same class of unsupported. Schema green, required keys present, and the next hop can still bounce. The fixture must write “card is legal” and “this operation is allowed by the flags” as two steps.

Layer four is traffic. You send a Task or a Message. You do not POST the Card again. InvalidAgentResponseError is about the response shape, not the business card. MIME must match defaultInputModes or a skill’s inputModes; a miss is ContentTypeNotSupportedError. Do not check an A2A skill with an MCP inputSchema. 1.0’s hints are examples and MIME types.

The generated bundle also carries snake_case patternProperties (supported_interfaces, default_input_modes). Spec JSON is camelCase. New cards follow the spec table. If CI accepts both key sets, Diff goes dirty first. Do not copy proto field names onto a public card unless you are deliberately running a compatibility layer and the fixture says so.

The Card: point at AgentCard, not the bundle root

Pin a copy of a2a.json in the repo, aligned with a published bundle version. CI should not hit a floating latest URL. Ajv (or any 2020-12 implementation) gets schema pointer #/$defs/AgentCard. The instance is the card you just GET, or agent-card.json in the repo. Emit path and keyword. If the path does not match the fixture, suspect the pointer before you suspect the card.

The first pass’s most valuable reject is an extra key. In 1.0 the endpoint sits in supportedInterfaces. A leftover top-level url, protocolVersion, or supportsAuthenticatedExtendedCard goes red under additionalProperties: false. That is 0.3 residue, not “more fields, safer.” The field tutorial already covered the move. Today we only require the validator to mark those keys as errors, not ignore them.

Each interface item needs three checks: production url is absolute HTTPS (gRPC is host:port), protocolBinding is a binding the client speaks, and protocolVersion is a protocol version such as 1.0 — not the agent’s own version. Both are called version. The fixture must assert them apart. The first entry is preferred. No shared binding, no call.

Skills: the spec requires fields the schema often skips

Generated AgentSkill often has no required either. A skill without tags may go green in Ajv and still have an empty search surface — the discovery post already said this. Today’s assert: every skill has a non-empty id, name, description, and at least one tag. An empty skills array fails the spec table. A NO_SPEC host-only entry should go red before registration, not after a keyword hop hits air.

A skill must not carry inputSchema. additionalProperties: false treats it as an extra key. That belongs to MCP tools; see the in-site Schema post. A 1.0 skill shows examples and MIME. Pour a function-parameter table into the card and the first pass should fail. Failure at validation is cheaper than clarifying after you send a Task.

The card below is meant to go red on pass one or pass two. It is not a “nearly usable” draft. It mixes a 0.3 top-level url, a skill with no tags, and an MCP-style inputSchema. Diff it next to Returns Specialist in the repo. The three reds should land on three paths.

{
  "name": "Returns Specialist",
  "description": "Classifies return requests.",
  "version": "1.0.3",
  "url": "https://agents.example.com/returns/a2a",
  "protocolVersion": "1.0",
  "supportedInterfaces": [
    {
      "url": "https://agents.example.com/returns/a2a",
      "protocolBinding": "JSONRPC",
      "protocolVersion": "1.0"
    }
  ],
  "capabilities": {
    "streaming": true
  },
  "defaultInputModes": ["text/plain"],
  "defaultOutputModes": ["text/plain"],
  "skills": [
    {
      "id": "classify-return",
      "name": "Classify a return",
      "description": "Decide the request type.",
      "inputSchema": {
        "type": "object",
        "required": ["orderId"],
        "properties": {
          "orderId": { "type": "string" }
        }
      }
    }
  ]
}
Check What failure looks like Next hop
Parse + AgentCard pointerTrailing comma; top-level url; skill inputSchemaFix JSON / drop 0.3 keys
Spec required / non-emptyEmpty name; skill without tags; empty skillsFill fields from §4.4
Flags vs operationSubscribe while streaming is false; fetch an undeclared extended cardChange the client, or change the Card flags

Traffic: a Task is not a business card

Only after the Card passes do you message/send. The body follows the binding — JSON-RPC, gRPC, or HTTP+JSON. The business object inside is a Message or a Task, not an AgentCard. Validate a request against the Card definition and it will fail in a way that teaches nothing. The generated bundle has separate Task, Message, Part, and Artifact defs. Traffic fixtures switch the pointer. Do not reuse the Card line.

Validate the response too. The spec folds a non-conforming agent reply into InvalidAgentResponseError. The state machine is submitted / working / completed / failed / canceled / rejected, plus interrupted input-required and auth-required. Treat completed as the only success and “please add one sentence” becomes an incident. Those keys are not on the Card. Discovery succeeded, the call failed — the ticket must name the layer.

The JSON below is a validation-report fixture, not an official catalog or A2A RPC. It folds the four layers into one object so you can diff it next to the repo Card and one message/send request. Signature checks only assert shape: each signatures[] item needs protected and signature (RFC 7515 JWS). Real keys and real verify stay in a security review. Do not put a private key in the fixture.

{
  "kind": "card-validation-report",
  "note": "CI/review fixture — not an official A2A RPC",
  "target": "agent-card.json",
  "schema": {
    "bundle": "https://a2a-protocol.org/latest/spec/a2a.json",
    "pointer": "#/$defs/AgentCard",
    "normative": false
  },
  "parse": true,
  "schemaPass": false,
  "schemaErrors": [
    { "path": "/url", "keyword": "additionalProperties" },
    { "path": "/skills/0/inputSchema", "keyword": "additionalProperties" }
  ],
  "specChecks": [
    { "id": "required-name", "pass": true },
    { "id": "skills-tags-nonempty", "pass": false },
    { "id": "no-top-level-url", "pass": false }
  ],
  "capability": {
    "streaming": true,
    "clientWillStream": true,
    "ok": true
  },
  "next": "fix-card"
}

Signatures, fixtures, local diffs

The spec allows signatures in RFC 7515 shape. If the array is present, first assert the two required strings are non-empty, then decide whether to verify. No array does not make the card illegal — the field is optional. Write the public card as if it will be fetched. Do not put a static secret or an intranet password in the Card. An extended card follows the session. Do not share one “already validated” cache with the public card.

Do not pour a Plugin SKILL.md or MCP tools/list into the same Card check. Skills inside the box are for a coding agent in this repo. The other team’s self-description is the Card. Smash both into one capability.json and three failed layers land on the same ticket line.

You can finish this in the browser: JSON format to see whether the card and the report parse; JSON Schema validate to point the official bundle at AgentCard, then switch the pointer for a Task; JSON Diff to compare the legal card with the red card and catch extra keys / missing tags. Data stays on this machine. Further reading: the Agent Card field tutorial, and the discovery walk. What a catalog is: the Registry overview.

Related: A2A Agent Card JSON Schema, How agents discover each other, Google Agent Registry.

FAQ

Ajv went green on a2a.json. Do I still need a second pass?

Yes. The site bundle calls itself non-normative, and generated defs often omit required. The spec table is the source of required. Green only means you did not trip extra keys and types.

Can one Schema validate both the Card and a Task?

No. Switch the pointer. AgentCard and Task are two $defs. Wrap a request in the Card and the failure message will send the next hop the wrong way.

May a skill hang a JSON Schema as its input?

A 1.0 skill does not take inputSchema. That is an MCP tool. The generated bundle treats it as an extra key. Deterministic parameters stay on the tool hop. Do not write them into the card.

Is a card illegal without signatures?

No. signatures is optional. If the array is present, assert the two JWS required strings. If it is absent, still run §4.4 required and the flags.

Takeaways and next steps

In 2026, “validate an Agent Card” folds into four layers: parse, generated-bundle pointer, spec required table, then flags and traffic. Catalog and discovery are the door in. Validation is the guard that decides whether you may delegate.

Ship in this order: pin a2a.json, pointer AgentCard; run the spec-table non-empty asserts; check flags and MIME before you send a Task; switch traffic fixtures to Task / Message. How to write the fields is the field tutorial. How you found the card is the discovery post. Diff the legal card, the red card, and the report in JSONVue.