Dealing with shape shifting data building dynamic tables in Filament
April 03, 2026
3 min read
In a perfect world, every database column is predefined, and every model is predictable. But in the real world, we often deal with EAV (Entity-Attribute-Value) patterns, JSONB metadata, or dynamic form builders where the user defines the fields.
If you try to map these to a standard Filament Table, you'll quickly realize that TextColumn::make('key') doesn't work when "key" changes for every user.
Here is how to build a "Cameleon Table" in Filament v5 that generates its schema on the fly.
The Problem: The Static Schema Trap
Imagine an IoT Monitoring System. Each device sends different telemetry data stored in a jsonb column called payload:
- Device A (Thermometer): {"temp": 22.5, "humidity": 45}
- Device B (GPS): {"lat": 44.4, "long": 26.1, "speed": 60}
If you want a single Filament Resource to show a table of all telemetry logs, you can't hardcode the columns. If you do, you'll end up with a mess of empty cells or a single, unreadable TextEntry showing the whole JSON blob.
The Solution: Programmatic Schema Injection
The beauty of Filament is that its table() and form() methods are just PHP. We can use logic to "inject" columns before the table renders.
Step 1: Prepare the Model
Ensure your JSON column is cast properly in your Laravel Model to allow easy collection manipulation.
1// App/Models/TelemetryLog.php2protected $casts = [3 'payload' => 'array', // or 'collection'4];
Step 2: Extract Unique Keys
To build the table headers, we first need to know what keys exist in the data. For performance, you might want to pluck these from a cache or a specific "Definition" model, but for this example, we’ll grab them from the latest records.
1// Inside TelemetryResource.php 2 3public static function getDynamicColumns(): array 4{ 5 // We fetch the keys from the last 50 entries to ensure we cover all types 6 return \App\Models\TelemetryLog::latest() 7 ->limit(50) 8 ->get() 9 ->flatMap(fn ($log) => array_keys($log->payload ?? []))10 ->unique()11 ->toArray();12}
Step 3: Map Keys to Filament Columns
Now, we transform those strings into actual Filament Column objects.
1public static function table(Table $table): Table 2{ 3 $dynamicKeys = self::getDynamicColumns(); 4 5 $columns = [ 6 // We still keep our static ID/Timestamp columns 7 Tables\Columns\TextColumn::make('created_at') 8 ->dateTime() 9 ->sortable(),10 Tables\Columns\TextColumn::make('device_id')11 ->searchable(),12 ];13 14 // Inject the dynamic columns15 foreach ($dynamicKeys as $key) {16 $columns[] = Tables\Columns\TextColumn::make("payload.{$key}")17 ->label(ucfirst($key))18 ->sortable()19 ->toggleable() // Highly recommended for dynamic tables!20 ->placeholder('N/A');21 }22 23 return $table24 ->columns($columns)25 ->filters([ /* ... */ ])26 ->actions([ /* ... */ ]);27}
The "Extreme" Hint: Type-Sensitive Columns
What if one JSON key is a boolean (status) and another is a number (price)? You can take this further by inspecting the value type of the first occurrence and returning a specific Filament Column type.
1foreach ($dynamicKeys as $key) {2 $firstValue = $sampleData->firstWhere($key)['value'];3 4 $columns[] = match(gettype($firstValue)) {5 'boolean' => Tables\Columns\IconColumn::make("payload.{$key}")->boolean(),6 'double', 'integer' => Tables\Columns\TextColumn::make("payload.{$key}")->numeric(),7 default => Tables\Columns\TextColumn::make("payload.{$key}")->limit(30),8 };9}
Performance Warning
Fetching 50 records every time the table loads just to get keys is expensive.
Pro Tip: Cache the dynamicKeys list and clear it only when a new type of data is ingested.
Database Tip: If using PostgreSQL, use a jsonb_object_keys query for much faster key extraction at scale.
Summary
By treating the columns() array as a dynamic object rather than a static list, you can build interfaces that adapt to your data in real-time. This turns Filament from a simple CRUD tool into a powerful, data-agnostic dashboard engine.