Filament v4 Schema-Based Forms and Infolists
Filament v4 introduced one of its most architecturally significant changes: a unified Schema API that collapses the previously separate form and infolist definition surfaces into a single, composable layer. If you have built Filament v3 resources, you know the friction — form(Form $form) and infolist(Infolist $infolist) each demanded their own component trees, leading to duplicated field definitions for read and write views.
Filament v4 solves this by making Schema the first-class citizen.
The Old Friction
In v3, a TextInput in a form and a TextEntry in an infolist were entirely separate classes with separate APIs. Keeping them in sync was a maintenance burden:
// v3 — two separate definitions, easy to drift
public static function form(Form $form): Form
{
return $form->schema([
TextInput::make('email')->email()->required(),
]);
}
public static function infolist(Infolist $infolist): Infolist
{
return $infolist->schema([
TextEntry::make('email'),
]);
}
The v4 Unified Schema
Filament v4 introduces Schema components that are context-aware: the same component tree renders as editable inputs inside a form context and as read-only entries inside an infolist context. You define the schema once and let Filament resolve the correct renderer.
use Filament\Schema\Schema;
use Filament\Schema\Components\TextInput;
use Filament\Schema\Components\Section;
public static function schema(Schema $schema): Schema
{
return $schema->components([
Section::make('Identity')
->schema([
TextInput::make('name')
->required()
->maxLength(120),
TextInput::make('email')
->email()
->required(),
]),
]);
}
Both the EditRecord and ViewRecord pages consume this single schema() method. The framework injects the correct context — form or infolist — at render time.
Reusable Schema Components
The real power emerges when you extract schemas into dedicated classes. This is the pattern I reach for on any resource with more than a handful of fields:
namespace App\Filament\Schemas;
use Filament\Schema\Schema;
use Filament\Schema\Components\TextInput;
use Filament\Schema\Components\Select;
class UserIdentitySchema
{
public static function make(): array
{
return [
TextInput::make('name')->required(),
TextInput::make('email')->email()->required(),
Select::make('role')
->options(Role::class)
->required(),
];
}
}
Then compose it anywhere:
public static function schema(Schema $schema): Schema
{
return $schema->components([
...UserIdentitySchema::make(),
...BillingAddressSchema::make(),
]);
}
This is clean, testable, and trivially shareable across resources, modals, and wizard steps.
Conditional Visibility
Conditional logic works identically whether the schema is rendered as a form or infolist:
TextInput::make('vat_number')
->visible(fn (Get $get): bool => $get('is_business') === true)
->required(fn (Get $get): bool => $get('is_business') === true),
The Get closure resolves live state in form context and static record state in infolist context — no branching required in your schema definition.
Practical Migration Note
If you are migrating from v3, the key shift is:
- Replace
form()+infolist()with a singleschema()method. - Swap
TextEntry,ImageEntry, etc. for their unifiedSchema\Componentsequivalents. - Move shared field groups into dedicated schema classes immediately — do not let them grow inline.
Not every v3 component has a direct v4 unified equivalent yet; check the official changelog before assuming a component is context-aware.
Takeaways
- Filament v4's unified Schema API eliminates the form/infolist duplication that plagued v3 resources.
- A single
schema()method serves both edit and view contexts via context injection. - Extracting schemas into plain PHP classes (
UserIdentitySchema::make()) is the cleanest reuse pattern. - Conditional visibility with
Getworks uniformly across both contexts. - Migrate incrementally: start with new resources, then refactor existing ones once you verify component parity.