Dev.to WebDev 🛠 Dev 👁 1 📖 7 min read

Episode 05: Structured Outputs in Laravel AI Applications

Structured output is the difference between asking a model to write something and asking it to return data Laravel can safely use. When an AI feature affects routing, storage, automation, or product decisions, the respo

Structured output is the difference between asking a model to write something and asking it to return data Laravel can safely use.

When an AI feature affects routing, storage, automation, or product decisions, the response needs a contract: fields, types, allowed values, validation rules, and a fallback path.

Plain text is useful for explanations, drafts, summaries, and human review. But when Laravel needs to classify a ticket, extract invoice fields, choose a workflow, or store a result, prose becomes fragile.

Structured output is not trust: A schema makes model output easier to parse. It does not make the model correct. Laravel still owns validation, authorization, record lookup, persistence, retries, and the final decision.

Why Plain Text Breaks Product Workflows

A paragraph can look right to a user and still be useless to the application.

If a model replies with "this is probably urgent billing," Laravel has to guess which value should go into the queue: urgent, high, billing, payment_issue, or something else.

That guess becomes hidden business logic.

Structured output removes that guess from the parser and moves it into an explicit contract. The model must return known fields. Laravel validates those fields before using them.

Use plain text when a human will read and approve the result.

Use structured output when Laravel will route, store, calculate, compare, or trigger another workflow.

Use both when the UI needs an explanation but the backend needs validated fields.

Start With the Application Decision

Do not begin by asking, "What JSON should the model return?"

Begin with the product decision Laravel needs to make.

A support triage feature may need priority, category, confidence, and a reason. An invoice extraction feature may need invoice number, vendor name, due date, currency, and line items.

Useful structured outputs usually fall into a few categories:

  • Classification: Choose from controlled labels such as priority, category, sentiment, moderation state, or next workflow.
  • Extraction: Pull fields from unstructured text, email, documents, or support messages, then validate them like external input.
  • Decision support: Return a recommendation plus confidence and reason, while Laravel keeps the final business decision.
  • UI structure: Return sections, bullets, table rows, or cards when a frontend needs predictable rendering.

A structured response should be as small as the decision requires. Extra fields increase cost, latency, validation work, and the chance that the model invents details.

Design a Schema Laravel Can Validate

A schema should describe the shape, types, allowed values, nullable behavior, and uncertainty rules.

The best schema is boring. It uses enums where the product has known choices and keeps free text short when the value will be stored or displayed.

<?php

namespace App\Ai\Contracts;

final class TicketTriageSchema
{
    public const PRIORITIES = ['low', 'normal', 'high', 'urgent'];
    public const CATEGORIES = ['billing', 'bug', 'account', 'feature_request', 'other'];

    public static function rules(): array
    {
        return [
            'priority' => ['required', 'string', 'in:low,normal,high,urgent'],
            'category' => ['required', 'string', 'in:billing,bug,account,feature_request,other'],
            'confidence' => ['required', 'numeric', 'between:0,1'],
            'reason' => ['required', 'string', 'max:180'],
        ];
    }
}

This kind of schema gives Laravel something concrete to enforce.

It also gives the model a clearer target: return one of these values, explain briefly, and express uncertainty as a number the application can reason about.

Prompt for Data, Not Decoration

Structured output prompts should be explicit about fields, allowed values, missing information, and what to do when the input is unclear.

Avoid asking for "valid JSON" only. That describes formatting, not business meaning.

$prompt = <<<PROMPT
Classify this support ticket for a Laravel support queue.

Allowed priorities: low, normal, high, urgent.
Allowed categories: billing, bug, account, feature_request, other.

Return JSON with exactly these fields:
- priority
- category
- confidence
- reason

Rules:
- confidence must be between 0 and 1.
- If the ticket is unclear, use category "other" and confidence below 0.6.
- Do not invent account state, payment status, or engineering cause.
- reason must be one short sentence.

Ticket:
{$ticket->message}
PROMPT;

The prompt defines the response contract, but Laravel must still parse and validate the result.

The model may ignore instructions, return malformed JSON, choose an unsupported enum, or include a confident but wrong reason.

Parse, Validate, Then Map to a DTO

Treat model output like an external API response.

Parse it carefully, validate it, and map it into a typed object before the rest of the application uses it.

Do not pass raw model arrays deep into your domain code.

<?php

namespace App\Services;

use App\Ai\Contracts\TicketTriageSchema;
use Illuminate\Support\Facades\Validator;
use JsonException;

final class RunTicketTriage
{
    public function parse(string $content): ?TicketTriageResult
    {
        try {
            $data = json_decode($content, true, flags: JSON_THROW_ON_ERROR);
        } catch (JsonException) {
            return null;
        }

        $validator = Validator::make($data, TicketTriageSchema::rules());

        if ($validator->fails()) {
            return null;
        }

        return new TicketTriageResult(
            priority: $data['priority'],
            category: $data['category'],
            confidence: (float) $data['confidence'],
            reason: $data['reason'],
        );
    }
}

A DTO is not only for cleanliness. It prevents the rest of the application from depending on model-specific response shapes.

If you change provider, schema, or parser later, the domain workflow can stay stable.

Use Confidence as a Routing Signal

Confidence should not be treated as truth. It is a routing signal.

A low-confidence classification can still be useful if Laravel uses it to slow down automation, ask for review, or choose a safer fallback.

if ($result === null) {
    return TicketTriageDecision::manualReview('Invalid AI output');
}

if ($result->confidence < 0.65) {
    return TicketTriageDecision::manualReview($result->reason);
}

if ($result->priority === 'urgent') {
    return TicketTriageDecision::escalate($result->category, $result->reason);
}

return TicketTriageDecision::route($result->category, $result->priority);

This is the key boundary: the model returns a structured suggestion, and Laravel decides what the suggestion is allowed to do.

The workflow can be conservative when confidence is low or when the category touches sensitive business rules.

Retry Only When It Can Help

If output is malformed, a retry with a repair prompt may help.

If the model chose the wrong business category because the ticket is ambiguous, retrying the same prompt may only add cost.

Separate formatting failures from uncertainty failures.

The structured output lifecycle usually looks like this:

  1. Prepare context: Laravel selects authorized and relevant fields.
  2. Request schema: Prompt or SDK asks for known fields and enums.
  3. Parse response: JSON or provider-native structured result is decoded.
  4. Validate: Laravel checks types, enums, ranges, and required fields.
  5. Map: Valid data becomes a DTO or value object.
  6. Decide: Laravel routes, stores, retries, or falls back.

Retry policy should be small and observable.

Log why the first response failed, how many retries were attempted, and whether the final output passed validation.

Otherwise structured output failures become invisible cost leaks.

Do Not Let Structured Output Skip Authorization

A structured response can look official. That is dangerous.

If the model returns { "action": "refund_customer" }, Laravel still must check whether the current user, tenant, policy, record state, and workflow allow that action.

  • Validate record IDs against the database before using them.
  • Check policies before showing or changing any record.
  • Use enums for allowed actions and deny unknown values.
  • Require human approval for sensitive actions such as refunds, account changes, or destructive operations.
  • Keep database writes outside the prompt and inside Laravel services or jobs.

A JSON field is not permission: Structured output may describe a requested action. It does not authorize that action. Permission still belongs to Laravel policies, services, and workflow rules.

Log the Contract, Not Just the Text

When structured output drives a workflow, logs should explain the contract that ran and the validation result.

This makes debugging much easier when a prompt version changes, a provider starts returning different formatting, or a schema becomes too strict.

logger()->info('ai.ticket_triage.structured_output', [
    'ticket_id' => $ticket->id,
    'schema' => 'ticket-triage:v1',
    'provider' => $response->provider,
    'model' => $response->model,
    'input_tokens' => $response->usage?->inputTokens,
    'output_tokens' => $response->usage?->outputTokens,
    'validation_passed' => $result !== null,
    'fallback' => $result === null ? 'manual_review' : null,
]);

Avoid logging sensitive prompt content by default.

In many applications, schema version, validation status, token usage, latency, and fallback path are enough for operational visibility.

Common Structured Output Mistakes

Most structured output bugs come from pretending the schema is stronger than it is.

The schema helps the model and the parser communicate, but the application still needs normal defensive engineering.

Common mistakes include:

  • Asking for JSON but not defining allowed values.
  • Letting the model return record IDs without verifying access and existence.
  • Using free-text categories instead of enums.
  • Failing open when parsing or validation fails.
  • Retrying every failure instead of distinguishing malformed output from ambiguous input.
  • Storing raw model output where typed application data is expected.
  • Changing schema fields without versioning logs and consumers.

Where Structured Outputs Fit in the Series

Structured output turns prompt engineering into an application contract.

It gives Laravel a predictable shape to parse, validate, log, and route.

It also makes the boundary between probabilistic generation and deterministic application behavior much clearer.

In the next episode, we will focus on reliability patterns for AI responses: validation, retry, fallback, repair prompts, manual review, and how Laravel can make model behavior dependable enough for production workflows.

If you found this useful, follow me for the next article in the AI Engineering with Laravel series.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.