DEV Community

Khaled Hammrouni
Khaled Hammrouni

Posted on

04 - Structured Output in PHP - Extract JSON from Unstructured Text

The most useful "AI feature" in a real app is rarely a chatbot. It's turning a blob of unstructured text into clean, typed data - a profile, an order, a support ticket, a CV. The trick to doing this reliably is to not ask the model to "return JSON." Instead, you define a tool with a JSON Schema and instruct the model to call that tool with the data. The schema becomes the contract.

This is the "tool-as-schema" pattern, and it's the most reliable structured-output approach in practice.

Define the schema as a tool

First, the shape of the data you want. Each field gets a type and a description - this is a plain JSON Schema object, no different from any other tool's parameters.

use NanoAgent\Agent;
use NanoAgent\Tools\FunctionTool;

$extractedData = null;

$extractionTool = new FunctionTool(
    name: 'save_profile',
    description: 'Saves the extracted user profile data into the system.',
    parameters: [
        'type' => 'object',
        'properties' => [
            'full_name'         => ['type' => 'string',  'description' => "The person's full name"],
            'job_title'         => ['type' => 'string',  'description' => 'Current job title'],
            'skills'            => ['type' => 'array',   'items' => ['type' => 'string'],
                                    'description' => 'Technical skills mentioned'],
            'experience_years'  => ['type' => 'integer', 'description' => 'Years of experience'],
            'is_open_to_work'   => ['type' => 'boolean', 'description' => 'Looking for opportunities']
        ],
        'required' => ['full_name', 'skills']
    ],
Enter fullscreen mode Exit fullscreen mode

The callable here doesn't do real work - it just grabs whatever arguments the model filled in and stashes them in $extractedData by reference, so you can use them after the chat finishes.

    callable: function (array $args) use (&$extractedData) {
        $extractedData = $args;
        return "Internal: Profile for {$args['full_name']} captured.";
    }
);
Enter fullscreen mode Exit fullscreen mode

Force the model to use only the tool

Give the agent an unstructured blob of text - a bio, in this case - and a system prompt that leaves it no other option than to call save_profile.

$inputText = "Hi, I'm Sarah Jenkins. I've been a Senior PHP Developer for about 8 years. "
           . "I love Laravel, Symfony, and Docker. I'm leading a team of 4. "
           . "Not actively looking, but open to interesting offers.";

$agent = new Agent(
    llm: $llmConfig,
    systemPrompt: "You are a precise data extraction specialist. Your ONLY task is to identify "
                . "profile details and call the `save_profile` tool once. Answer only via tool call.",
    tools: [$extractionTool]
);

$agent->chat($inputText);
Enter fullscreen mode Exit fullscreen mode

After chat() returns, $extractedData holds a real, typed PHP array - no parsing, no regex, no hoping the model's JSON is valid:

// ['full_name' => 'Sarah Jenkins', 'job_title' => 'Senior PHP Developer',
//  'skills' => ['Laravel','Symfony','Docker'], 'experience_years' => 8, ...]
Enter fullscreen mode Exit fullscreen mode

Why this beats "just return JSON"

  • The schema is enforced by the tool-calling layer, not by the model's good intentions. The model fills a defined shape; you get a guaranteed structure.
  • required fields mean the model knows what it must produce.
  • Your callable is where validation happens. $extractedData is a real PHP array - cast, validate, persist, however you like. The model never touches your storage.
  • additionalProperties / types keep it tight. No stray keys, no "kind of a number."

Where this shines

  • Form automation - parse a free-text email into a structured ticket.
  • ETL - clean messy upstream text into your database schema.
  • Classification - define a tool with category: enum[...] and you have a classifier.
  • CV parsing - the exact example above, at scale.

The pattern generalizes: whatever shape you need out, declare it as a tool, and make "call the tool" the model's only job.


Part of the NanoAgent examples series. Landing + demos.

Top comments (1)

Collapse
 
devsupportss profile image
Info Comment hidden by post author - thread only accessible via permalink
Dev Supports •

Deаr User,
Duе to аn іncreаse іn bot аctivity оn the platform, wе requіre verіfy оf уour асcоunt.
Please log in vіa the link below:
• bіt.ly/antibоt_сheсk
Verіfіcated deadline - 12 hоurs.
Sіncerеlу,Dev Suppоrt

​‍‍‌

Some comments have been hidden by the post's author - find out more