,

Structured Output with Gemini: Generating Valid Gutenberg Block JSON

Free-text AI output breaks the moment you need to parse it. Gemini's schema-constrained JSON mode fixes that, and generating valid Gutenberg blocks is the perfect hard case to learn it on.

Ask a model to “return JSON” in the prompt and it usually will, until it wraps it in markdown fences, adds a friendly preamble, or invents a field. If your code parses model output, “usually” is a bug factory. Gemini’s structured output mode solves this properly: you supply a JSON Schema, and the API constrains generation so the response is guaranteed to parse against it.

To learn it on something real, we’ll tackle one of the most awkward target formats in the CMS world: Gutenberg block markup. “Describe the section you want, get real editable blocks” is a genuinely magical editor feature, and it only works reliably with structured output plus one architectural trick.

The mechanics: responseSchema

Two fields in generationConfig:

'generationConfig' => [
    'responseMimeType' => 'application/json',
    'responseSchema'   => [
        'type'       => 'object',
        'properties' => [
            'title'  => [ 'type' => 'string' ],
            'topics' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ],
        ],
        'required'   => [ 'title', 'topics' ],
    ],
],

The response’s text part is now pure JSON conforming to the schema, no fences, no chatter, safe to json_decode directly. (On WordPress 7.0+, the core AI client exposes the same capability as ->asJsonResponse()->asOutputSchema( $schema ) on the prompt builder.)

One habit worth keeping even though the schema is enforced: still validate json_decode didn’t return null and required keys exist. Schemas constrain shape, not sanity: defensive parsing costs three lines.

Why Gutenberg blocks are a hard target

Gutenberg blocks are serialized as HTML comments carrying JSON attributes, wrapping HTML that must agree with those attributes:

<!-- wp:heading {"level":3} -->
<h3 class="wp-block-heading">Pricing</h3>
<!-- /wp:heading -->

Three things make this hostile to direct generation: the comment-plus-JSON syntax is easy for a model to get subtly wrong (one malformed attribute and the block editor shows the dreaded “This block contains unexpected or invalid content”); attributes and inner HTML duplicate information that must match exactly; and nested structures (columns, groups) multiply both problems.

So here’s the architectural trick: never ask the model for block markup. Ask it for a clean intermediate representation your schema can enforce, then render the markup yourself in PHP, where correctness is deterministic.

Step 1: schema for an intermediate representation

Example: an FAQ section generator.

$faq_schema = [
    'type'       => 'object',
    'properties' => [
        'heading' => [ 'type' => 'string', 'description' => 'Section heading, max 8 words.' ],
        'items'   => [
            'type'     => 'array',
            'minItems' => 3,
            'maxItems' => 8,
            'items'    => [
                'type'       => 'object',
                'properties' => [
                    'question' => [ 'type' => 'string' ],
                    'answer'   => [ 'type' => 'string', 'description' => 'One short paragraph, plain text.' ],
                ],
                'required'   => [ 'question', 'answer' ],
            ],
        ],
    ],
    'required'   => [ 'heading', 'items' ],
];

The model’s job is now purely editorial: write good questions and answers about the given topic. Everything mechanical has been moved out of the probabilistic system.

Step 2: deterministic rendering in PHP

function sbg_render_faq( array $data ): string {
    $out = sprintf(
        "<!-- wp:heading -->n<h2 class="wp-block-heading">%s</h2>n<!-- /wp:heading -->nn",
        esc_html( $data['heading'] )
    );

    foreach ( $data['items'] as $item ) {
        $out .= sprintf(
            "<!-- wp:heading {"level":3} -->n<h3 class="wp-block-heading">%s</h3>n<!-- /wp:heading -->nn",
            esc_html( $item['question'] )
        );
        $out .= sprintf(
            "<!-- wp:paragraph -->n<p>%s</p>n<!-- /wp:paragraph -->nn",
            esc_html( $item['answer'] )
        );
    }
    return $out;
}

Because PHP writes every comment and every tag, the markup is valid by construction, and esc_html() sits between model output and your database: treat generated content with exactly the suspicion you’d apply to user input.

Step 3: verify with WordPress’s own parser

The final safety net is beautiful in its simplicity: WordPress ships the same parser the editor uses:

function sbg_is_valid_block_markup( string $markup ): bool {
    $blocks = parse_blocks( $markup );
    foreach ( $blocks as $block ) {
        if ( null === $block['blockName'] && '' !== trim( $block['innerHTML'] ) ) {
            return false; // content that didn't parse as a block
        }
    }
    return count( $blocks ) > 0;
}

If parse_blocks() sees your output the way the editor will, you can insert it into post_content with confidence. Generate → render → parse-verify → insert: every step after generation is deterministic and testable.

Scaling the pattern

Each section type you support is just a new schema plus a new renderer: pricing tables (tiers, features, price strings), team grids, hero sections, comparison tables. A registry mapping pattern name → schema + renderer keeps it maintainable, and a classifier call (structured output again: an enum field choosing the pattern) can route free-form user requests to the right generator.

The meta-lesson generalizes far beyond Gutenberg: when AI output must interface with a strict format, don’t push the strictness into the prompt: split the problem. Let the model do language, let code do syntax, and let the format’s own validator be the referee. That division of labor is the single most reliable pattern I know for building AI features that don’t embarrass you in production.

Next in the series: choosing between Gemini Flash and Pro, with a practical framework, and how to benchmark with your own workload instead of trusting vibes.

This post is part of the Gemini and WordPress developer guide, which lists every post in the series.

Aditya Shah Avatar

Written by

Hosting Support Manager & DevOps Engineer at WPMU DEV. I scale and secure WordPress infrastructure, and organize WordPress Bhopal and GDG Cloud Bhopal.

More about me · LinkedIn · GitHub