AI agents · JSON Schema · TypeScript

Typed Agent Artifacts with JSON Schema and TypeScript

Create versioned plans, reports, patches, and decisions that are validated at runtime instead of trusting model-generated prose.

Published
Updated
Code examples
Illustrative
Reading time
5 minutes

Typed agent artifacts are versioned data contracts for work products such as plans, research reports, tool proposals, and review decisions. TypeScript checks code at build time; JSON Schema validates the untrusted JSON produced by a model or remote agent at runtime. You need both, plus semantic checks for rules a schema cannot express.

This guide builds a framework-neutral artifact envelope and shows where validation stops being proof.

Use artifacts for deliverables, not every thought

A useful artifact has a producer, schema version, creation time, payload, and provenance. It is durable enough to validate, store, diff, and hand to another component.

Do not turn hidden reasoning into a required field. Store observable work products:

  • a plan with steps and dependencies;
  • a research report with claims and source references;
  • a proposed action with exact tool arguments;
  • a review with criterion-level findings;
  • a patch description with affected files and verification evidence.

The current A2A specification distinguishes Message communication from Artifact outputs and says results should be returned as artifacts rather than status messages. It also warns that messages are not reliable delivery for critical information. That separation is useful even inside a single process. A2A protocol specification

Start with a versioned envelope

interface ArtifactEnvelope<TType extends string, TPayload> {
  readonly id: string;
  readonly type: TType;
  readonly schemaVersion: string;
  readonly runId: string;
  readonly producer: {
    readonly component: string;
    readonly version: string;
  };
  readonly createdAt: string;
  readonly payload: TPayload;
  readonly evidence: readonly EvidenceRef[];
}

interface EvidenceRef {
  readonly id: string;
  readonly source: string;
  readonly digest?: string;
  readonly retrievedAt?: string;
}

Keep schemaVersion independent from the agent or application version. A new prompt may produce the same schema; a breaking payload change needs a new schema version even if the agent name stays unchanged.

Define the runtime schema

This example uses JSON Schema Draft 2020-12 for a research-report payload:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://example.invalid/schemas/research-report/1-0-0",
  "type": "object",
  "required": ["question", "claims", "limitations"],
  "properties": {
    "question": { "type": "string", "minLength": 1 },
    "claims": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["text", "evidenceIds", "confidence"],
        "properties": {
          "text": { "type": "string", "minLength": 1 },
          "evidenceIds": {
            "type": "array",
            "items": { "type": "string", "minLength": 1 },
            "minItems": 1,
            "uniqueItems": true
          },
          "confidence": {
            "type": "string",
            "enum": ["low", "medium", "high"]
          }
        },
        "additionalProperties": false
      }
    },
    "limitations": {
      "type": "array",
      "items": { "type": "string" }
    }
  },
  "additionalProperties": false
}

Draft 2020-12 defines the core and validation vocabularies, including assertion keywords for structural validation. Declare the dialect explicitly because keywords and tuple semantics differ between drafts. JSON Schema Draft 2020-12, validation vocabulary

Use a validator that explicitly supports the declared dialect. Compile trusted schemas at application startup, not model-supplied schemas at request time. Limit input size and validation error output so deeply nested or enormous payloads cannot consume unbounded resources.

Keep TypeScript aligned without pretending it validates JSON

The compile-time shape can mirror the schema:

interface ResearchClaim {
  readonly text: string;
  readonly evidenceIds: readonly string[];
  readonly confidence: "low" | "medium" | "high";
}

interface ResearchReport {
  readonly question: string;
  readonly claims: readonly ResearchClaim[];
  readonly limitations: readonly string[];
}

const exampleReport = {
  question: "Which protocol fits this boundary?",
  claims: [],
  limitations: ["No production traffic was measured."],
} satisfies ResearchReport;

TypeScript's satisfies operator checks that an expression is compatible with a type without changing the expression's inferred type. It does not validate a string parsed at runtime. TypeScript 4.9 release notes

Choose one source of truth and generate the other representation where your toolchain supports it. Whichever direction you choose, add a CI fixture suite containing valid and invalid examples. Generation can still lose semantics when TypeScript and JSON Schema features do not map exactly.

Add semantic validation after structural validation

The example schema can prove that every claim has at least one evidence ID. It cannot prove that:

  • the evidence exists or belongs to the same tenant;
  • the source supports the claim;
  • confidence is calibrated;
  • two claims do not contradict each other;
  • a URL resolved when the artifact was created; or
  • the report answers the user's actual question.

Write a second validation layer:

interface ValidationIssue {
  readonly path: string;
  readonly code: string;
  readonly message: string;
}

function validateResearchSemantics(
  report: ResearchReport,
  knownEvidenceIds: ReadonlySet<string>,
): readonly ValidationIssue[] {
  const issues: ValidationIssue[] = [];

  report.claims.forEach((claim, index) => {
    for (const evidenceId of claim.evidenceIds) {
      if (!knownEvidenceIds.has(evidenceId)) {
        issues.push({
          path: "/claims/" + index + "/evidenceIds",
          code: "unknown_evidence",
          message: "Evidence " + evidenceId + " is not in this run",
        });
      }
    }
  });

  return issues;
}

For consequential actions, semantic validation must also call authorization policy. A well-formed request to delete another tenant's data remains forbidden.

Repair narrowly and visibly

When model output fails validation:

  1. retain the raw output in protected diagnostic storage if policy permits;
  2. return a bounded list of machine-readable errors to the model;
  3. allow a small, fixed number of repair attempts;
  4. validate the entire replacement artifact again; and
  5. fail visibly when the budget is exhausted.

Do not silently coerce "false" to false, discard unknown action parameters, or invent missing evidence. Coercion can change meaning. For a UI-only optional field it may be acceptable; for money, recipients, permissions, or deletion targets it is not.

Version contracts deliberately

Use semantic versioning rules for meaning even if the version string is not SemVer:

  • adding an optional field is usually backward-compatible;
  • making an optional field required is breaking;
  • narrowing an enum is breaking for existing producers;
  • changing a field's meaning is breaking even if its JSON type is unchanged;
  • adding an enum value can break consumers that assume exhaustive handling.

Readers must reject unsupported major versions. During migration, accept old and new versions at the boundary, normalize them into an internal representation, and keep emitted versions explicit. Never guess an unrecognized version.

Bind approvals and signatures to canonical bytes

If an artifact represents an approved action, hash a canonical representation of every consequential field. Ordinary JSON.stringify output should not be assumed to be a cross-language signing format. RFC 8785 defines the JSON Canonicalization Scheme for repeatable hashing and signing, with strict constraints including deterministic property sorting. It is an Informational RFC rather than an Internet Standards Track specification; adopt it only if all participants implement its data constraints and verified errata. RFC 8785, RFC 8785 errata

approval_subject = SHA-256(
  canonicalize({
    artifactType,
    schemaVersion,
    tool,
    arguments,
    tenantId,
    policyVersion
  })
)

Store the digest, approver identity, timestamp, expiry, and decision. Recompute immediately before execution. Any parameter change invalidates the approval.

Security and operational failure modes

FailureWhy the schema does not prevent itControl
Prompt injection in a stringString is structurally validTreat content as data; enforce tool policy in code
Cross-tenant evidenceID has the correct typeAuthorization-scoped lookup
Huge nested payloadMay validate eventuallyRequest, depth, array, and time limits
Hallucinated citationURL is a valid stringResolve, archive revision metadata, inspect support
Schema downgradeOld JSON is validMinimum accepted version and explicit negotiation
Approval replayDigest is valid twiceOne-time intent ID, expiry, and execution ledger

OWASP recommends structured validation but pairs it with least privilege, memory isolation, output controls, and human approval for high-impact operations. Schema validation is one layer, not the security boundary. OWASP AI Agent Security Cheat Sheet

Artifact contract checklist

  • Use artifacts only for durable, observable deliverables.
  • Declare a JSON Schema dialect and stable schema identifier.
  • Validate untrusted JSON before using it as typed data.
  • Add authorization and domain-specific semantic checks.
  • Reject unknown consequential fields instead of silently dropping them.
  • Bound size, depth, validation time, and repair attempts.
  • Preserve producer version, run ID, provenance, and evidence references.
  • Test old readers against new writers before compatible changes ship.
  • Canonicalize before hashing across systems.
  • Bind high-impact execution to an unexpired, exact artifact digest.