Repository navigation
Description
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 usingResolveA2uiProps<InferredComponentApiSchemaType<Api>>. WithA2uiSchema<T>, this automatically resolves toTwith 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 usingInferredComponentApiSchemaType<Api>. WithA2uiSchema<T>,this.propsis automatically typed asT:// a2ui-controller.ts in Lit renderer public props: ResolveA2uiProps<InferredComponentApiSchemaType<Api>>;
-
Angular (
@a2ui/angular):
Angular'sComponentApiToProps<Api>utility currently usesz.infer<Api['schema']>. We update Angular'score/types.tsto importInferredComponentApiSchemaType<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.) extendingBasicCatalogComponent<typeof TextApi>receivesthis.props()typed asComponentApiToProps<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]
- Payload Arrival (
MessageProcessor):
AnUpdateComponentsMessagearrives in@a2ui/web_core'sMessageProcessorcontaining unvalidated JSON properties (rawProperties). - Schema Validation (
A2uiSchema.validate):
MessageProcessorcallscomponentApi.schema.validate(rawProperties). Whether the schema under the hood is Zod 3, Zod 4, Valibot, or static JSON Schema, it validates the data and returnsValidationResult<T>. - Model Update (
ComponentModel):
MessageProcessorassignsvalidationResult.data(now safely typed asT) toComponentModel.properties. - Renderer Binding & Rendering:
- React:
GenericBinderreadsComponentModel.properties(typeT), resolves data-binding expressions againstDataContext, and passes the resolvedpropsobject into<Component {...props} />. - Lit:
A2uiControllerreadsComponentModel.properties(typeT), resolves data-bindings viaGenericBinder, and triggers Lit's reactive render cycle withthis.props. - Angular:
ComponentBinderreadsComponentModel.properties(typeT), updates the corresponding reactive Angular Signals (BoundProperty<T>), and triggersOnPushchange detection inCatalogComponent<Api>.
- React:
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_catalogto 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-zod3MUST be vended simultaneously with the core release so existing Zod 3 users have an immediate drop-in migration path (zod3Schema(...)).@a2ui/schema-zod4is 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.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsTodo
Activity
ditman commentedon Aug 4, 2026
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_corerenderers/web_core/src/v0_9/schema/schema-interface.tsValidationResult<T = any>,A2uiSchema<T = any>, andInferSchemaType<S extends A2uiSchema>.renderers/web_core/src/v0_9/index.ts./schema/schema-interface.js.Step 2: Refactor catalog definitions in
@a2ui/web_corerenderers/web_core/src/v0_9/catalog/types.tsz.ZodTypeAnywithA2uiSchemainComponentApi,InferredComponentApiSchemaType, andFunctionApi.Catalog.invoker, replacefn.schema.parse(rawArgs)with:Step 3: Refactor message processing in
@a2ui/web_corerenderers/web_core/src/v0_9/processing/message-processor.tszodandzod-to-json-schema.generateInlineCatalog(), replacezodToJsonSchema(api.schema, {target: 'jsonSchema2019-09'})withapi.schema.toJsonSchema().processUpdateComponentsMessage(), replacecomponentApi.schema.safeParse(properties)withcomponentApi.schema.validate(properties).Step 4: Create Zod 3 adapter package (
@a2ui/schema-zod3)renderers/schema/zod3/"renderers/schema/zod3"to the"workspaces"array in the rootpackage.json.renderers/schema/zod3/package.json"@a2ui/schema-zod3""zod": "^3.25.0","zod-to-json-schema": "^3.24.0","@a2ui/web_core": "workspace:*"AGENTS.md(build,lint,lint:fix,test,format,format:check).renderers/schema/zod3/src/index.tsZod3Schema<T>andexport function zod3Schema<T>(schema: z.ZodType<T>): A2uiSchema<T>.Step 5: Update
basic_catalogin@a2ui/web_corebasic_catalogremains inweb_core, update component and function schema definitions inrenderers/web_core/src/v0_9/basic_catalog/to wrap Zod 3 definitions withzod3Schema(...).basic_catalogis converted to zero dependencies, replacez.object(...)definitions withcreateStaticSchema<T>(...).Step 6: Connect Angular renderer (
@a2ui/angular)renderers/angular/src/v0_9/core/types.tsz.infer<Api['schema']>withInferredComponentApiSchemaType<Api>imported from@a2ui/web_core/v0_9.zodas a direct dependency inrenderers/angular/package.json.Step 7: Update CHANGELOGs and document breaking changes
renderers/web_core/CHANGELOG.md### Breaking Changesentry documenting thatComponentApi.schema,FunctionApi.schema, and catalog definitions now expectA2uiSchema<T>instead ofz.ZodTypeAny.zod3Schema(...)from the newly vended@a2ui/schema-zod3package.renderers/angular/CHANGELOG.md### Breaking Changesentry documenting that@a2ui/angularno longer depends directly onzod, and that custom Angular catalog components/providers now infer prop types viaInferredComponentApiSchemaType<Api>.Step 8: Verification commands
Execute the following verification suite from the workspace root:
All tests and type checks must pass without errors.
aren55555 commentedon Sep 22, 2026
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
a2uibut 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.nan-yu commentedon Sep 25, 2026
A2UI Triage: Thanks @aren55555 for the feedback on Cashew Systems' migration to Zod 4. Abstracting the Zod dependency into
@a2ui/schema-zod3remains a priority on our P2 roadmap to unblock ecosystems on Zod 4 while maintaining internal compatibility.ditman commentedon Sep 28, 2026
We also need this internally, for example if we wanted to produce a typescript demo that uses ADK for typescript, which requires Zod4.
louis-bompart commentedon Oct 22, 2026
@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 😁
feat(web_core): let the app pass its protocol and envelope schemas
refactor(web_core): generate the protocol types from the specification
fix(web_core): drop the unused private types of the Zod modules
refactor(web_core)!: move the Zod 3 schemas to @a2ui/schema-zod3
refactor(schema-zod3)!: install the Zod 3 library only on request
yjbanov commentedon Oct 29, 2026
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.
ditman commentedon Nov 2, 2026
@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!