Schema Discovery – fluent-wp-client
Overview
Section titled “Overview”Schema discovery lets you fetch the JSON Schema WordPress exposes for any resource or ability, then use it as a validator, type source, or AI tool input schema — without hard-coding shapes upfront.
Two paths serve different use cases:
| Approach | Best for | Type safety | Requires rebuild? |
|---|---|---|---|
Runtime (explore() + zodFromJsonSchema) | Multi-site apps, dynamic clients, AI agents | Runtime only | No |
CLI (fluent-wp-client schemas) | Single-site apps, Astro, CI pipelines | Compile-time | Yes, on schema changes |
flowchart TD WP[WordPress REST API\nOPTIONS + ability schemas] Normalize[Catalog normalization\n- bool -> boolean\n- int -> integer\n- date-time formats relaxed\n- optional ACF fields allow empty strings\n- update schemas have no required fields] Catalog[Serializable discovery catalog\ncontent, terms, resources, abilities]
WP --> Runtime[Runtime: wp.explore()] WP --> CLI[Build/CI: fluent-wp-client schemas] Runtime --> Normalize CLI --> Normalize Normalize --> Catalog
Catalog --> Cache[KV, file, database\nJSON.stringify / structuredClone] Cache --> Restore[wp.useCatalog(restoredCatalog)] Catalog --> Client[Client helpers\ndescribe(), getJsonSchema(), getQueryParams()] Restore --> Client Catalog --> Generated[Generated artifacts\nJSON Schema, Zod schemas, TypeScript types]
Client --> Use[Use in apps and agents\nforms, Valibot/Zod conversion, AI tool schemas] Generated --> UseBoth paths produce the same normalized catalog shape. Runtime discovery stores that catalog on the client; the CLI writes artifacts from the same discovery data for checked-in or CI-generated use.
Describe a resource
Section titled “Describe a resource”Call .describe() on any resource client to get its JSON Schemas:
const desc = await wp.content('books').describe();
desc.schemas.item // single item response shapedesc.schemas.collection // collection response shapedesc.schemas.create // accepted fields for createdesc.schemas.update // accepted fields for update (no required fields)Use getJsonSchema() when you only need one schema variant from a fluent resource client:
const createSchema = await wp.content('books').getJsonSchema('create');const updateSchema = await wp.content('books').getJsonSchema('update');Works the same for terms, first-class resources, and abilities:
await wp.terms('categories').describe();await wp.media().describe();await wp.users().describe();await wp.comments().describe();await wp.settings().describe();await wp.ability('my-plugin/send-notification').describe();Runtime schema conversion with zodFromJsonSchema
Section titled “Runtime schema conversion with zodFromJsonSchema”The fluent-wp-client/zod subpackage exports helpers that convert discovered JSON Schemas to Zod validators at runtime. This is the primary path for apps that manage multiple WordPress instances or build client configuration dynamically.
import { zodFromJsonSchema } from 'fluent-wp-client/zod';
const desc = await wp.content('books').describe();const createSchema = zodFromJsonSchema(desc.schemas.create!);
const validated = createSchema!.parse({ title: { raw: 'My Book' }, status: 'publish',});
const book = await wp.content('books').create(validated);import { zodSchemasFromDescription } from 'fluent-wp-client/zod';
const catalog = await wp.explore();const bookSchemas = zodSchemasFromDescription(catalog.content.books);
// bookSchemas.item — validate a read response// bookSchemas.create — validate a create payload// bookSchemas.update — validate an update payloadimport { zodSchemasFromDescription } from 'fluent-wp-client/zod';
const catalog = await wp.explore();const calcSchemas = zodSchemasFromDescription(catalog.abilities['acme/calculate']);
const input = calcSchemas.input!.parse({ amount: 100, currency: 'USD' });const result = await wp.ability('acme/calculate').run(input);Explore: full catalog
Section titled “Explore: full catalog”wp.explore() returns schemas for all content types, taxonomies, first-class resources, and abilities in one call. The result is a plain serializable object — safe for JSON.stringify, structuredClone, and cache storage.
const catalog = await wp.explore();
// catalog.content — post types (posts, pages, books, …)// catalog.terms — taxonomies (categories, tags, …)// catalog.resources — media, users, comments, settings// catalog.abilities — registered WordPress abilities// catalog.site — site metadata, such as timezone when authenticatedLimit the scope with include, or bust the in-memory cache with refresh:
await wp.explore({ include: ['content', 'terms'] });await wp.explore({ refresh: true });Available kinds: 'content', 'terms', 'resources', 'abilities'.
Cache and restore with useCatalog()
Section titled “Cache and restore with useCatalog()”The catalog is serializable — persist it to any KV store, file, or database and restore it later to skip the discovery round-trip:
const catalog = await wp.explore();await kv.set('wp:catalog', JSON.stringify(catalog));
// Later — restore without network callsconst restored = JSON.parse(await kv.get('wp:catalog') ?? 'null');if (restored) { wp.useCatalog(restored);}
// describe() now returns from cacheconst desc = await wp.content('posts').describe();After useCatalog(), all .describe() calls (including ability descriptions) return cached data without a network round-trip.
Build-time schema generation with the CLI
Section titled “Build-time schema generation with the CLI”Use the CLI when your app talks to a single WordPress instance and you want checked-in schema files with compile-time type safety:
npx fluent-wp-client schemas \ --url https://example.com \ --zod-out src/lib/wp-schemas.ts \ --json-out src/lib/wp-schemas.jsonThe generated TypeScript file contains JSON Schema literals, Zod schemas via z.fromJSONSchema(), and inferred types:
import { wpBookSchema, type WPBook } from './lib/wp-schemas';
const book = wpBookSchema.parse(rawData);Choosing runtime vs CLI
Section titled “Choosing runtime vs CLI”Runtime (explore() → zodFromJsonSchema()):
- WordPress instance determined at runtime (multi-site, user-provided URL)
- AI tools that adapt to the connected site’s capabilities
- No rebuild needed when WordPress schema changes
- No compile-time type inference (types are
unknownat build time)
CLI (fluent-wp-client schemas):
- WordPress instance known at build time (single site)
- Astro content collections, CI validation, shared app schemas
- Full compile-time type safety via
z.infer - Must regenerate when WordPress schema changes
Both approaches start from the same WordPress REST schema source and apply the same durable normalization before the schema is used by the client, cached, or emitted as artifacts.
Astro example
Section titled “Astro example”Astro actions require native Zod validators at module scope — CLI-generated schema files are the natural fit:
import { defineAction } from 'astro:actions';import { WordPressClient } from 'fluent-wp-client';import { postsCreateSchema } from '../lib/wp-schemas';
const wp = new WordPressClient({ baseUrl: import.meta.env.WP_URL, auth: { username: import.meta.env.WP_USER, password: import.meta.env.WP_APP_PASSWORD, },});
export const server = { createPost: defineAction({ input: postsCreateSchema, handler: async (input) => { return wp.content('posts').create(input); }, }),};AI SDK example
Section titled “AI SDK example”Use runtime discovery when you want the model to adapt to the current site before sending a mutation:
import { generateObject } from 'ai';import { openai } from '@ai-sdk/openai';import { zodFromJsonSchema } from 'fluent-wp-client/zod';
const desc = await wp.content('posts').describe();const createSchema = zodFromJsonSchema(desc.schemas.create!);
const { object } = await generateObject({ model: openai('gpt-4o-mini'), schema: createSchema!, prompt: 'Draft a short post announcing our April product update.',});
await wp.content('posts').create(object);Multi-site example
Section titled “Multi-site example”When your app manages multiple WordPress instances, runtime conversion lets each client adapt to its instance:
import { WordPressClient } from 'fluent-wp-client';import { zodSchemasFromDescription } from 'fluent-wp-client/zod';
async function createPostOnSite(siteUrl: string, auth: { username: string; password: string }) { const wp = new WordPressClient({ baseUrl: siteUrl, auth }); const catalog = await wp.explore({ include: ['content'] }); const schemas = zodSchemasFromDescription(catalog.content.posts);
const validated = schemas.create!.parse({ title: { raw: 'Cross-site post' }, status: 'draft', });
return wp.content('posts').create(validated);}Authentication
Section titled “Authentication”const wp = new WordPressClient({ baseUrl: 'https://example.com', auth: { username: 'admin', password: process.env.WP_APP_PASSWORD },});
const usersSchema = await wp.users().describe();const settingsSchema = await wp.settings().describe();Exported types and helpers
Section titled “Exported types and helpers”import type { WordPressDiscoveryCatalog, WordPressDiscoveryOptions, WordPressJsonSchema, WordPressResourceDescription, WordPressAbilityDescription,} from 'fluent-wp-client';import { zodFromJsonSchema, zodSchemasFromDescription, type ResourceZodSchemas, type AbilityZodSchemas,} from 'fluent-wp-client/zod';