Skip to main content
The DataModel system in LarAgent provides a robust foundation for handling structured data. It ensures strict typing, automatic validation, serialization, and OpenAPI schema generation - flexible enough for simple DTOs while powerful enough for complex, nested, and polymorphic data structures. Used in storage, tool arguments and structured output with LLMs, the DataModel system is a core part of LarAgent’s architecture.

Core Features

All data models extend the LarAgent\Core\Abstractions\DataModel abstract class, which provides:

Automatic Hydration

Populate objects from arrays using fill() or fromArray()

Schema Generation

Automatically generate OpenAPI/JSON Schemas using PHP types and attributes

Serialization

Convert objects back to arrays or JSON

Performance

Uses static runtime caching to minimize Reflection overhead

Basic Usage

When creating a DataModel for use with LLMs (structured output or tool arguments), use the #[Desc] attribute to provide context that helps the AI understand the purpose and expected format of each field.
use LarAgent\Core\Abstractions\DataModel;
use LarAgent\Attributes\Desc;

class SearchQuery extends DataModel
{
    #[Desc('The search term to look for')]
    public string $query;

    #[Desc('The maximum number of results to return')]
    public int $limit = 10;
}
// Create from array
$query = SearchQuery::fromArray([
    'query' => 'Laravel AI agents',
    'limit' => 5
]);

echo $query->query; // 'Laravel AI agents'

// Generate JSON schema automatically
$schema = $query->toSchema();
Even when you don’t need LLM integration, you can still use DataModel instead of a plain DTO to benefit from the built-in fromArray() and toArray() methods.

Polymorphic Arrays

To handle lists of different model types (e.g., a message containing both text and images), extend DataModelArray. DataModelArray allows you to define a set of allowed models and a discriminator field to determine which model to instantiate for each item in the array. It’s like a collection, but strictly typed to only allow specific DataModels based on a discriminator field.
use LarAgent\Core\Abstractions\DataModelArray;

class MessageContent extends DataModelArray
{
    // Define allowed types and their mapping
    public static function allowedModels(): array
    {
        return [
            'text' => TextContent::class,
            'image_url' => ImageContent::class,
        ];
    }

    // Define the field used to distinguish types (default is 'type')
    public function discriminator(): string
    {
        return 'type';
    }
}
You can instantiate polymorphic arrays in multiple ways:
// From array
$content = new MessageContent([
    ['type' => 'text', 'text' => 'Hello'],
    ['type' => 'image_url', 'image_url' => ['url' => 'https://example.com/img.png']]
]);

// Or using variadic objects
$content = new MessageContent(
    new TextContent(['text' => 'Hello']),
    new ImageContent(['image_url' => ['url' => 'https://example.com/img.png']])
);

Differentiation

Sometime one discriminator field is not enough to differentiate between multiple models. In such cases, you can override the matchesArray() method in DataModel. For example, in a chat message array where both AssistantMessage and ToolCallMessage share the same role value of assistant, you can implement custom logic in matchesArray to check for the presence of specific fields:
class MessageArray extends DataModelArray
{
    public static function allowedModels(): array
    {
        return [
            'user' => UserMessage::class,
            'system' => SystemMessage::class,
            'developer' => DeveloperMessage::class,
            'tool' => ToolResultMessage::class,
            'assistant' => [
                ToolCallMessage::class,  // Check first (has specific condition via matchesArray)
                AssistantMessage::class, // Default fallback
            ],
        ];
    }

    public function discriminator(): string
    {
        return 'role';
    }
}
In ToolCallMessage & AssistantMessage, you can implement custom logic:
ToolCallMessage.php

    // ...

    /**
     * Check if array data matches ToolCallMessage (has tool_calls).
     */
    public static function matchesArray(array $data): bool
    {
        return ! empty($data['tool_calls']);
    }

AssistantMessage.php

    // ...

    /**
     * Check if array data matches AssistantMessage (no tool_calls).
     */
    public static function matchesArray(array $data): bool
    {
        return empty($data['tool_calls']);
    }

Where the $data is the raw array being hydrated. This allows you to implement any complex differentiation logic based on the presence/absence or value of certain fields.

Supported Property Types

DataModel supports various types for properties that are automatically mapped to Schema types, automatically validated, and serialized:

Basic Types

string, int, float, bool

Nullable Types

?string, ?int, etc. - marks property as optional in schema

Arrays

array - simple arrays without type hints

Nested Models

Other DataModel classes as properties

Enums

PHP Enum for constrained values

Data Model Arrays

DataModelArray for typed collections

Nested Data Models

DataModels can contain other DataModels as properties, enabling you to build complex, hierarchical data structures. Nested models are automatically hydrated when using fromArray() and properly serialized with toArray().
use LarAgent\Core\Abstractions\DataModel;
use LarAgent\Attributes\Desc;

class ImageContent extends DataModel
{
    #[Desc('The type of the content')]
    public string $type = 'image_url';

    #[Desc('The image URL information')]
    public ImageUrl $image_url;
}

// Nested DataModel
class ImageUrl extends DataModel
{
    #[Desc('The URL of the image')]
    public string $url;

    #[Desc('The detail level for image processing')]
    public string $detail = 'auto';
}
// Nested hydration works automatically
$content = ImageContent::fromArray([
    'type' => 'image_url',
    'image_url' => [
        'url' => 'https://example.com/image.png',
        'detail' => 'high'
    ]
]);

echo $content->image_url->url; // 'https://example.com/image.png'

Enums

PHP backed enums are fully supported and automatically converted to JSON Schema enum constraints. This is useful when a property should only accept specific values.
use LarAgent\Core\Abstractions\DataModel;
use LarAgent\Attributes\Desc;

enum Sentiment: string
{
    case Positive = 'positive';
    case Neutral = 'neutral';
    case Negative = 'negative';
}

enum Priority: int
{
    case Low = 1;
    case Medium = 2;
    case High = 3;
}

class ContentAnalysis extends DataModel
{
    #[Desc('The sentiment of the analyzed content')]
    public Sentiment $sentiment;

    #[Desc('Priority level for follow-up')]
    public Priority $priority;

    #[Desc('Summary of the content')]
    public string $summary;
}
// Usage
$analysis = ContentAnalysis::fromArray([
    'sentiment' => 'positive',  // or Sentiment::Positive
    'priority' => 2,            // or Priority::Medium
    'summary' => 'Great product review'
]);

echo $analysis->sentiment->value; // 'positive'
echo $analysis->priority->value;  // 2

// Schema generation includes enum values
$schema = $analysis->toSchema();
// The 'sentiment' property will have: "enum": ["positive", "neutral", "negative"]
Use string-backed enums for human-readable values that the LLM can easily understand. Integer-backed enums work well for numeric scales or priority levels.

DataModelArray as Property

A DataModelArray can be used as a property within a DataModel, enabling typed collections of polymorphic items.
use LarAgent\Core\Abstractions\DataModel;
use LarAgent\Core\Abstractions\DataModelArray;
use LarAgent\Attributes\Desc;

// Define the polymorphic array
class MessageContent extends DataModelArray
{
    public static function allowedModels(): array
    {
        return [
            'text' => TextContent::class,
            'image_url' => ImageContent::class,
        ];
    }

    public function discriminator(): string
    {
        return 'type';
    }
}

// Use it as a property in another DataModel
class ChatMessage extends DataModel
{
    #[Desc('The role of the message sender')]
    public string $role;

    #[Desc('The content of the message, which can include text and images')]
    public MessageContent $content;
}
// Hydration works automatically for nested DataModelArrays
$message = ChatMessage::fromArray([
    'role' => 'user',
    'content' => [
        ['type' => 'text', 'text' => 'What is in this image?'],
        ['type' => 'image_url', 'image_url' => ['url' => 'https://example.com/photo.jpg']]
    ]
]);

// Access the typed content
foreach ($message->content as $item) {
    if ($item instanceof TextContent) {
        echo $item->text;
    } elseif ($item instanceof ImageContent) {
        echo $item->image_url->url;
    }
}

// Serialization preserves the structure
$array = $message->toArray();
When a DataModelArray is used as a property, it behaves like any nested DataModel—automatically hydrated from arrays and serialized back to arrays.

DTO-Style Data Models

For simple data transfer objects that don’t need schema generation (For example, when you use it only in storage), you can use a lightweight DTO approach:
It’s good practice to override fromArray() and toArray() methods for DataModels that are heavily used. Static method resolution is faster than the dynamic reflection-based approach used by default, which can make a significant difference at scale.
use LarAgent\Core\Abstractions\DataModel;

class SessionIdentity extends DataModel
{
    public function __construct(
        public readonly string $agentName,
        public readonly ?string $chatKey = null,
        public readonly ?string $userId = null,
        public readonly ?string $group = null
    ) {}

    public static function fromArray(array $data): self
    {
        return new self(
            agentName: $data['agentName'],
            chatKey: $data['chatKey'] ?? '',
            userId: $data['userId'] ?? '',
            group: $data['group'] ?? ''
        );
    }

    public function toArray(): array
    {
        return [
            'agentName' => $this->agentName,
            'chatKey' => $this->chatKey,
            'userId' => $this->userId,
            'group' => $this->group,
        ];
    }
}

Performance Optimization

The DataModel class uses Reflection to inspect properties and types. While LarAgent implements static runtime caching to mitigate the cost, Reflection is still slower than native code.
You are not required to override fromArray() or toArray() methods. The base implementation works perfectly for 90% of use cases.

When to Override

Override fromArray() and toArray() only if:
  1. The model is instantiated frequently (thousands of times in a loop)
  2. The model is part of a core hot path (like MessageContent in a streaming response)
  3. You need custom transformation logic that standard casting doesn’t support

Performance-Optimized Example

Premature optimization can make your code harder to maintain. Start with the default implementation and optimize only when necessary.
use LarAgent\Core\Abstractions\DataModel;
use LarAgent\Attributes\Desc;

class ImageContent extends DataModel
{
    #[Desc('The type of the content')]
    public string $type = 'image_url';

    #[Desc('The image URL information')]
    public ImageUrl $image_url;

    // Override for performance (bypasses Reflection)
    public function toArray(): array
    {
        return [
            'type' => $this->type,
            'image_url' => $this->image_url->toArray(),
        ];
    }

    // Override for performance
    public static function fromArray(array $attributes): static
    {
        $instance = new static();
        
        if (isset($attributes['type'])) {
            $instance->type = $attributes['type'];
        }
        
        if (isset($attributes['image_url'])) {
            // Handle nested hydration manually
            $instance->image_url = is_array($attributes['image_url'])
                ? ImageUrl::fromArray($attributes['image_url'])
                : $attributes['image_url'];
        }
        
        return $instance;
    }
}

Summary

ApproachUse CaseDescription Required
Property PromotionSimple DTOsNo
Public PropertiesAPI mappingRecommended
With #[Desc]LLM interactionsYes
Custom fromArray/toArrayHigh-performance pathsOptional
DataModelArrayPolymorphic collectionsDepends on contents