Course 5 used as_json_response() once, for a two-field product description object.
This lesson goes further: a bulk-tagging feature that needs three fields back from a
single post, a title suggestion, an array of tags, and a category that has to be
one of the categories that actually exist on the site, not one the model invents. That
last constraint is exactly where a loosely-specified JSON request falls apart, and
exactly where a properly built JSON Schema earns its keep.
Course 5’s structured output example and Lesson 1’s system instruction patterns. This
lesson assumes a working wp_ai_client_prompt() call already returning text.
Step 1: Why a bare JSON request isn’t enough
Asking for JSON without a schema, in the underlying SDK that’s asOutputMimeType( 'application/json' ) on its own, tells the model to format its answer as JSON, but says
nothing about what fields belong in that JSON, what types they should be, or what values
are allowed. A model left to guess the shape will still produce valid JSON, just not
necessarily the JSON your code expects, an extra field one run, a missing one the next,
category spelled categories on a bad day. as_json_response() accepts an optional
schema argument specifically to close that gap:
// File: my-plugin/class-bulk-tagger.php
$schema = array(
'type' => 'object',
'properties' => array(
'title' => array( 'type' => 'string' ),
'tags' => array(
'type' => 'array',
'items' => array( 'type' => 'string' ),
'maxItems' => 5,
),
'category' => array( 'type' => 'string' ),
),
'required' => array( 'title', 'tags', 'category' ),
);
Step 2: Constraining category to values that actually exist
A free-text category string still lets the model invent a category your site doesn’t
have. Building the schema’s enum from the site’s real registered categories closes
that gap entirely, the model can only pick from what you give it:
// File: my-plugin/class-bulk-tagger.php
$categories = get_categories( array( 'hide_empty' => false ) );
$category_names = wp_list_pluck( $categories, 'name' );
$schema = array(
'type' => 'object',
'properties' => array(
'title' => array( 'type' => 'string' ),
'tags' => array(
'type' => 'array',
'items' => array( 'type' => 'string' ),
'maxItems' => 5,
),
'category' => array(
'type' => 'string',
'enum' => $category_names,
),
),
'required' => array( 'title', 'tags', 'category' ),
);
Building the enum from get_categories() at request time, rather than hardcoding a list
in the schema, means the constraint automatically stays correct as categories are added
or renamed on the site, no separate list to remember to update.
Step 3: Calling as_json_response() with the schema
// File: my-plugin/class-bulk-tagger.php
$post_content = wp_strip_all_tags( get_post_field( 'post_content', $post_id ) );
$raw = wp_ai_client_prompt()
->with_text(
"Post content:\n" . wp_trim_words( $post_content, 400 )
)
->using_system_instruction(
'Suggest a concise title, up to five relevant tags, and the single best-fit ' .
'category for this WordPress post. Only choose a category from the allowed ' .
'list, never invent a new one.'
)
->as_json_response( $schema )
->using_temperature( 0.2 )
->generate_text();
A low temperature suits this task well, bulk tagging benefits from consistent, repeatable categorization far more than from variety.
Step 4: Parsing the result defensively
Requesting a schema is a strong instruction to the model, and on many providers it’s enforced quite tightly at the API level, but it isn’t a guarantee your PHP code should skip validating. Always check the decode succeeded and the fields you need are present before writing anything to the database:
// File: my-plugin/class-bulk-tagger.php
$data = json_decode( $raw, true );
if (
json_last_error() !== JSON_ERROR_NONE
|| ! is_array( $data )
|| empty( $data['title'] )
|| empty( $data['category'] )
|| ! in_array( $data['category'], $category_names, true )
) {
// Lesson 6 covers what to do next: retry with clarification, or fall back safely.
return null;
}
wp_update_post( array(
'ID' => $post_id,
'post_title' => sanitize_text_field( $data['title'] ),
) );
wp_set_post_terms( $post_id, $data['tags'] ?? array(), 'post_tag' );
wp_set_post_terms( $post_id, array( $data['category'] ), 'category' );
Checking in_array( $data['category'], $category_names, true ) again here, even though
the schema already constrained it, costs almost nothing and protects against the rare
case where a provider’s schema enforcement is looser than expected.
A schema with properties but no required array tells the model every field is
optional, which invites it to omit tags entirely on a post it considers hard to
categorize rather than returning an empty array. Always list every field your code
actually depends on in required, not just the ones that feel essential.
Test it: run the bulk-tagging call
wp-env run cli wp eval '
$categories = get_categories( array( "hide_empty" => false ) );
$category_names = wp_list_pluck( $categories, "name" );
$schema = array(
"type" => "object",
"properties" => array(
"title" => array( "type" => "string" ),
"tags" => array( "type" => "array", "items" => array( "type" => "string" ), "maxItems" => 5 ),
"category" => array( "type" => "string", "enum" => $category_names ),
),
"required" => array( "title", "tags", "category" ),
);
echo wp_ai_client_prompt()
->with_text( "Post content:\nA guide to setting up automated backups for a WordPress site using WP-CLI cron jobs." )
->using_system_instruction( "Suggest a concise title, up to five relevant tags, and the single best-fit category. Only choose from the allowed category list." )
->as_json_response( $schema )
->using_temperature( 0.2 )
->generate_text();
'
Expect output shaped like:
{
"title": "Automating WordPress Backups With WP-CLI Cron Jobs",
"tags": ["backups", "wp-cli", "automation", "cron"],
"category": "Tutorials"
}
with category always one of the names actually returned by get_categories() on your
site.
Recap
as_json_response() accepts an optional JSON Schema, and that schema is what turns “the
model will probably return something JSON-shaped” into “the model returns exactly the
fields your code expects, with values constrained to what’s actually valid on this
site.” Build constraints like the category enum from live WordPress data rather than a
hardcoded list, mark every field your code depends on as required, and always parse
the result defensively, a schema request lowers the odds of bad output, it doesn’t
eliminate the need to check for it.
Resources & further reading
- PHP AI Client SDK repository, GitHub
- Introducing the AI Client in WordPress 7.0, make.wordpress.org
- get_categories() function reference, developer.wordpress.org