Skip to content

[FEATURE]: Abstract dependency on Zod on web_core. #2160

Open

Description

@ditman

A2UI Schema Abstraction & Zod Decoupling Plan

  • I have searched the existing issues to make sure this feature has not already been requested.

Is your feature request related to a problem? Please describe.

Currently, @a2ui/web_core depends on zod: ^3.25.X. This is a problem because Zod 3 is in maintenance mode/EOL, and ideally it should be using zod 4 at least, however Google does not have zod v4 available internally yet, so we still need to support zod3.

Similarly to what we did with @a2ui/markdown-it (where internal google wants a different rendering library), we should abstract our dependency on Zod by some interface that can be implemented using different libraries; zod3 (to preserve compatibility with existing code), zod4 (for future-proofing), and others... ideally simultaneously to allow for different catalog implementers to select their own.


Describe the Proposed Solution

Core interface design (A2uiSchema<T>)

We replace z.ZodTypeAny across @a2ui/web_core with A2uiSchema<T = any>.

Types

/**
 * Result of a validation operation, modeled as a TypeScript discriminated union.
 * - When `success` is true, `.data` is strongly typed as `T`.
 * - When `success` is false, `.errors` contains validation error messages or issues.
 */
export type ValidationResult<T = any> =
  | { success: true; data: T }
  | { success: false; errors: string[] | unknown };

/**
 * Framework-agnostic schema interface for validating data and generating JSON schemas.
 */
export interface A2uiSchema<T = any> {
  /** Phantom type marker for compile-time TypeScript inference. */
  readonly _type?: T;

  /**
   * Human-readable description of the schema.
   * Used by `MessageProcessor.generateInlineCatalog()` to populate LLM function descriptions
   * in `InlineCatalog` client capability reports sent to the server.
   */
  readonly description?: string;

  /** Validates data without throwing an exception. */
  validate(data: unknown): ValidationResult<T>;

  /** Converts this schema into a standard JSON Schema object for InlineCatalog reporting. */
  toJsonSchema(): Record<string, unknown>;
}

/** Extracts the inferred TypeScript type from an A2uiSchema at compile time. */
export type InferSchemaType<S extends A2uiSchema> =
  S extends A2uiSchema<infer T> ? T : never;

Catalog definitions

/**
 * Definition of a UI component's API contract in an A2UI catalog.
 * Defines the component's name and property schema independent of any rendering implementation.
 *
 * @template Schema - The A2uiSchema type describing the component's properties.
 */
export interface ComponentApi<Schema extends A2uiSchema = A2uiSchema> {
  /** The unique name of the component as it appears in A2UI JSON (e.g., 'Button'). */
  name: string;
  /** The schema describing and validating this component's properties. */
  readonly schema: Schema;
}

/**
 * Type helper that extracts the static TypeScript property interface from a ComponentApi.
 * Used by renderers and catalogs to access component property types with compile-time safety.
 */
export type InferredComponentApiSchemaType<Api extends ComponentApi> =
  Api extends ComponentApi<infer Schema> ? InferSchemaType<Schema> : never;

/**
 * Definition of a callable function's API contract in an A2UI catalog.
 */
export interface FunctionApi {
  /** The unique name of the function (e.g., 'openUrl'). */
  readonly name: string;
  /** The expected return type category ('string', 'number', 'boolean', 'object', etc.). */
  readonly returnType: A2uiReturnType;
  /** The schema describing and validating the function's parameter arguments. */
  readonly schema: A2uiSchema;
}

How @a2ui/web_core connects to renderers: Static props typing & runtime dataflow

Client renderers (@a2ui/angular, @a2ui/react, @a2ui/lit) connect to @a2ui/web_core at two distinct levels: compile-time static props inference and runtime validated property dataflow.

1. Compile-time static props inference (InferredComponentApiSchemaType<Api>)

All three client renderers deduce the static TypeScript interface of a component's properties using InferredComponentApiSchemaType<Api> from @a2ui/web_core.

Under the new architecture, InferredComponentApiSchemaType<Api> inspects Api['schema']['_type'] at compile time and evaluates to the underlying TypeScript interface T:

  • React (@a2ui/react):
    React components define their prop types using ResolveA2uiProps<InferredComponentApiSchemaType<Api>>. With A2uiSchema<T>, this automatically resolves to T with zero code changes:

    // adapter.tsx in React renderer
    type Props = ResolveA2uiProps<InferredComponentApiSchemaType<Api>>;
  • Lit (@a2ui/lit):
    Lit components define their controller and host element types using InferredComponentApiSchemaType<Api>. With A2uiSchema<T>, this.props is automatically typed as T:

    // a2ui-controller.ts in Lit renderer
    public props: ResolveA2uiProps<InferredComponentApiSchemaType<Api>>;
  • Angular (@a2ui/angular):
    Angular's ComponentApiToProps<Api> utility currently uses z.infer<Api['schema']>. We update Angular's core/types.ts to import InferredComponentApiSchemaType<Api> from @a2ui/web_core:

    -export type ComponentApiToProps<Api extends ComponentApi> = InferredInterfaceToProps<
    -  ExtendedProps<z.infer<Api['schema']>>
    ->;
    +export type ComponentApiToProps<Api extends ComponentApi> = InferredInterfaceToProps<
    +  ExtendedProps<InferredComponentApiSchemaType<Api>>
    +>;

    With this one-line diff, every Angular component (TextComponent, ButtonComponent, etc.) extending BasicCatalogComponent<typeof TextApi> receives this.props() typed as ComponentApiToProps<typeof TextApi>, where each property is wrapped in an Angular Signal (BoundProperty<T>) with 100% static compile-time safety and zero Zod imports.

2. Runtime validated property dataflow

At runtime, properties flow from the server to the actual renderer render methods in a 4-step pipeline:

flowchart LR
    A[Server JSON Message] --> B[MessageProcessor]
    B -->|api.schema.validate| C[ComponentModel.properties]
    C --> D[Renderer Binders]
    D -->|Props / Signals| E[UI Render Methods]
Loading
  1. Payload Arrival (MessageProcessor):
    An UpdateComponentsMessage arrives in @a2ui/web_core's MessageProcessor containing unvalidated JSON properties (rawProperties).
  2. Schema Validation (A2uiSchema.validate):
    MessageProcessor calls componentApi.schema.validate(rawProperties). Whether the schema under the hood is Zod 3, Zod 4, Valibot, or static JSON Schema, it validates the data and returns ValidationResult<T>.
  3. Model Update (ComponentModel):
    MessageProcessor assigns validationResult.data (now safely typed as T) to ComponentModel.properties.
  4. Renderer Binding & Rendering:
    • React: GenericBinder reads ComponentModel.properties (type T), resolves data-binding expressions against DataContext, and passes the resolved props object into <Component {...props} />.
    • Lit: A2uiController reads ComponentModel.properties (type T), resolves data-bindings via GenericBinder, and triggers Lit's reactive render cycle with this.props.
    • Angular: ComponentBinder reads ComponentModel.properties (type T), updates the corresponding reactive Angular Signals (BoundProperty<T>), and triggers OnPush change detection in CatalogComponent<Api>.

Runtime validation & multi-catalog interoperability

Catalogs using different schema libraries (Zod 3, Zod 4, Valibot, or static JSON Schema) can interoperate within the same application:

<A2uiProvider catalogs={[ValibotCatalog, Zod3Catalog, ZeroDepBasicCatalog]} />

Validating mixed-catalog responses in MessageProcessor

When an UpdateComponentsMessage arrives containing components from multiple catalogs, MessageProcessor validates each component independently against its respective catalog schema:

for (const instance of message.components) {
  const [componentType, rawProperties] = Object.entries(instance.component)[0];

  const componentApi = surface.catalog.components.get(componentType);
  if (!componentApi) {
    throw new A2uiValidationError(`Unknown component type '${componentType}'`);
  }

  // Validate against the component's specific schema
  const validationResult = componentApi.schema.validate(rawProperties);

  // (Optionally) Throw on error when parsing
  if (!validationResult.success) {
    throw new A2uiValidationError(
      `Validation failed for '${componentType}'`,
      validationResult.errors,
    );
  }

  // ValidationResult.data is of known type T
  surfaceModel.setComponentProperties(instance.id, componentType, validationResult.data);
}

Basic catalog schemas

Currently, the basic catalog uses zod3. In order to keep web_core free of dependencies on zod, we can:

  • Separate the basic_catalog to a separate package with the dependencies that it needs (preferred)
  • Implement a JSON Schema by hand with a simple validation

(Optional) Static JSON-Schema basic catalog

basic_catalog components could use static A2uiSchema<T> objects backed by prebuilt JSON Schema definitions and a noop validator:

export function createStaticSchema<T>(
  jsonSchema: Record<string, unknown>,
  validator?: (data: unknown) => ValidationResult<T>,
): A2uiSchema<T> {
  return {
    validate: validator ?? ((data) => ({ success: true, data: data as T })),
    toJsonSchema: () => jsonSchema,
  };
}

Adapter packages (@a2ui/schema-zod3 & future @a2ui/schema-zod4)

Converting ComponentApi.schema from z.ZodTypeAny to A2uiSchema is a breaking change for custom catalog authors.

  • @a2ui/schema-zod3 MUST be vended simultaneously with the core release so existing Zod 3 users have an immediate drop-in migration path (zod3Schema(...)).
  • @a2ui/schema-zod4 is optional and can be added in a later release as Zod 4 adoption grows, or as a sample for others' to implement their own schema adapters.

Zod 3 adapter (@a2ui/schema-zod3)

Implements A2uiSchema using zod/v3 and zod-to-json-schema.

import type {z} from 'zod/v3';
import {zodToJsonSchema} from 'zod-to-json-schema';
import type {A2uiSchema, ValidationResult} from '@a2ui/web_core';

export class Zod3Schema<T> implements A2uiSchema<T> {
  constructor(private readonly zodSchema: z.ZodType<T>) {}

  get description(): string | undefined {
    return this.zodSchema.description;
  }

  validate(data: unknown): ValidationResult<T> {
    const result = this.zodSchema.safeParse(data);
    if (result.success) {
      return {success: true, data: result.data};
    }
    return {
      success: false,
      errors: result.error.errors.map(e => `${e.path.join('.')}: ${e.message}`),
    };
  }

  toJsonSchema(): Record<string, unknown> {
    return zodToJsonSchema(this.zodSchema, {
      target: 'jsonSchema2019-09',
    }) as Record<string, unknown>;
  }
}

/** Wraps a Zod 3 schema into an A2uiSchema. */
export function zod3Schema<T>(schema: z.ZodType<T>): A2uiSchema<T> {
  return new Zod3Schema(schema);
}

Custom catalog authoring example

This is how a component API would use the zod3Schema builder:

import {z} from 'zod';
import {zod3Schema} from '@a2ui/schema-zod3';
import type {ComponentApi} from '@a2ui/web_core';

export const CustomCardApi: ComponentApi = {
  name: 'CustomCard',
  schema: zod3Schema(
    z.object({
      title: z.string(),
      elevated: z.boolean().optional(),
    }),
  ),
};

Describe Alternatives Considered

I've tried supporting both zod3 and zod4 by broadening the dependency that we have on zod, but we end up needing some unsavory code to differentiate between zod3 and 4 at runtime.

Activity

  1. added
    status: needs-triageauto-managed: https://github.com/a2ui-project/a2ui/blob/main/scripts/triage.mjs
    on Aug 3, 2026
  2. removed
    status: needs-triageauto-managed: https://github.com/a2ui-project/a2ui/blob/main/scripts/triage.mjs
    on Aug 4, 2026
  3. ditman commented on Aug 4, 2026

    @ditman
    CollaboratorAuthor

    Some directives for agents to help tackle this. These are valid now, but things change quickly so take with a grain of salt later.

    Agent Implementation Specification & Checklist

    This section provides explicit repository paths, package manifests, code replacements, and verification commands so an AI agent can implement this feature autonomously.

    Step 1: Define core interface in @a2ui/web_core

    • File to create: renderers/web_core/src/v0_9/schema/schema-interface.ts
      • Define and export ValidationResult<T = any>, A2uiSchema<T = any>, and InferSchemaType<S extends A2uiSchema>.
    • File to modify: renderers/web_core/src/v0_9/index.ts
      • Re-export all types from ./schema/schema-interface.js.

    Step 2: Refactor catalog definitions in @a2ui/web_core

    • File to modify: renderers/web_core/src/v0_9/catalog/types.ts
      • Replace z.ZodTypeAny with A2uiSchema in ComponentApi, InferredComponentApiSchemaType, and FunctionApi.
      • In Catalog.invoker, replace fn.schema.parse(rawArgs) with:
        const validationResult = fn.schema.validate(rawArgs);
        if (!validationResult.success) {
          throw new A2uiExpressionError(`Validation failed for function '${name}'`, name, validationResult.errors);
        }
        return fn.execute(validationResult.data, ctx, abortSignal);

    Step 3: Refactor message processing in @a2ui/web_core

    • File to modify: renderers/web_core/src/v0_9/processing/message-processor.ts
      • Remove imports of zod and zod-to-json-schema.
      • In generateInlineCatalog(), replace zodToJsonSchema(api.schema, {target: 'jsonSchema2019-09'}) with api.schema.toJsonSchema().
      • In processUpdateComponentsMessage(), replace componentApi.schema.safeParse(properties) with componentApi.schema.validate(properties).

    Step 4: Create Zod 3 adapter package (@a2ui/schema-zod3)

    • Directory to create: renderers/schema/zod3/
    • Root registration: Add "renderers/schema/zod3" to the "workspaces" array in the root package.json.
    • Package manifest: renderers/schema/zod3/package.json
      • Package name: "@a2ui/schema-zod3"
      • Dependencies: "zod": "^3.25.0", "zod-to-json-schema": "^3.24.0", "@a2ui/web_core": "workspace:*"
      • Scripts: Implement canonical targets per AGENTS.md (build, lint, lint:fix, test, format, format:check).
    • File to create: renderers/schema/zod3/src/index.ts
      • Implement Zod3Schema<T> and export function zod3Schema<T>(schema: z.ZodType<T>): A2uiSchema<T>.

    Step 5: Update basic_catalog in @a2ui/web_core

    • Option A (Separate Package / Zod 3 Adapter): If basic_catalog remains in web_core, update component and function schema definitions in renderers/web_core/src/v0_9/basic_catalog/ to wrap Zod 3 definitions with zod3Schema(...).
    • Option B (Zero-Dependency Static Schemas): If basic_catalog is converted to zero dependencies, replace z.object(...) definitions with createStaticSchema<T>(...).

    Step 6: Connect Angular renderer (@a2ui/angular)

    • File to modify: renderers/angular/src/v0_9/core/types.ts
      • Replace z.infer<Api['schema']> with InferredComponentApiSchemaType<Api> imported from @a2ui/web_core/v0_9.
    • File to check: Verify Angular components compile without requiring zod as a direct dependency in renderers/angular/package.json.

    Step 7: Update CHANGELOGs and document breaking changes

    • File to modify: renderers/web_core/CHANGELOG.md
      • Add a ### Breaking Changes entry documenting that ComponentApi.schema, FunctionApi.schema, and catalog definitions now expect A2uiSchema<T> instead of z.ZodTypeAny.
      • Explain the migration path: import and wrap Zod 3 schemas using zod3Schema(...) from the newly vended @a2ui/schema-zod3 package.
    • File to modify: renderers/angular/CHANGELOG.md
      • Add a ### Breaking Changes entry documenting that @a2ui/angular no longer depends directly on zod, and that custom Angular catalog components/providers now infer prop types via InferredComponentApiSchemaType<Api>.
      • Explain the migration path for applications using custom catalogs with Zod 3.

    Step 8: Verification commands

    Execute the following verification suite from the workspace root:

    yarn build
    yarn workspace @a2ui/web_core test
    yarn workspace @a2ui/schema-zod3 test
    yarn workspace @a2ui/angular test
    yarn workspace @a2ui/lit test
    yarn workspace @a2ui/react test

    All tests and type checks must pass without errors.

  4. aren55555 commented on Sep 22, 2026

    @aren55555

    This feature would be huge for us at @cashew-systems -- because of the Zod 3 EOL, we've been migrating everything to Zod 4 and are almost complete. We would like to use a2ui but we don't want to have to downgrade part of our system to Zod 3. Like at Google, I've instituted that our company only have one version of each dependency, as it makes my life checking for vulnerabilities a lot easier across our monorepo.

    Our approach for now is to maintain our own private fork of a2ui, but it would be better if we could stay with the main branch.

  5. added
    status: needs-triageauto-managed: https://github.com/a2ui-project/a2ui/blob/main/scripts/triage.mjs
    on Sep 22, 2026
  6. nan-yu commented on Sep 25, 2026

    @nan-yu
    Collaborator

    A2UI Triage: Thanks @aren55555 for the feedback on Cashew Systems' migration to Zod 4. Abstracting the Zod dependency into @a2ui/schema-zod3 remains a priority on our P2 roadmap to unblock ecosystems on Zod 4 while maintaining internal compatibility.

  7. removed
    status: needs-triageauto-managed: https://github.com/a2ui-project/a2ui/blob/main/scripts/triage.mjs
    on Sep 25, 2026
  8. ditman commented on Sep 28, 2026

    @ditman
    CollaboratorAuthor

    We also need this internally, for example if we wanted to produce a typescript demo that uses ADK for typescript, which requires Zod4.

  9. louis-bompart commented on Oct 22, 2026

    @louis-bompart

    @ditman, I think the idea of abstracting Zod from the Schema is a good idea 💯. It'll add a good chunk of flexibility (we too would like to use A2UI with Zod4 ;) )
    I am wondering, however, if it's best to vendor an A2UI abstraction (the A2uiSchema<T> or if it'd be best to rely & delegate that concern to an opinionated standard used by the schema library, namely Standard Schema.

    Leaving this here as food for thought 😁

  10. added
    status: needs-triageauto-managed: https://github.com/a2ui-project/a2ui/blob/main/scripts/triage.mjs
    on Oct 22, 2026
  11. yjbanov commented on Oct 29, 2026

    @yjbanov
    Collaborator

    Update: Another use-case is to use protobuf as transport encoding instead of JSON, and protobuf has its own validation system. This is still being discussed.

  12. removed
    status: needs-triageauto-managed: https://github.com/a2ui-project/a2ui/blob/main/scripts/triage.mjs
    on Oct 29, 2026
  13. ditman commented on Nov 2, 2026

    @ditman
    CollaboratorAuthor

    I am wondering, however, if it's best to vendor an A2UI abstraction (the A2uiSchema or if it'd be best to rely & delegate that concern to an opinionated standard used by the schema library, namely Standard Schema.

    @louis-bompart oooh, this looks like a good thing. I was thinking of having a minimal-dependency type system for this, but I'll consider this standard schema thing as well, thanks for pointing it out!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions