Why the Schema API Exists
In Filament v3 you maintained two parallel trees: form(Form $form) returned a Form wrapping Components\*, and infolist(Infolist $infolist) returned an Infolist wrapping Entries\*. The same field — say, a user's email — needed two separate definitions that drifted apart over time.
Filament v4 collapses this into a unified Schema. One component tree can render as an editable form or a read-only infolist depending on context. The practical payoff is a single source of truth for layout, validation hints, and conditional visibility.
The New Method Signatures
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Select;
use Filament\Infolists\Components\TextEntry;
class UserResource extends Resource
{
public static function schema(Schema $schema): Schema
{
return $schema->components([
TextInput::make('name')
->required()
->maxLength(255),
TextInput::make('email')
->email()
->required(),
Select::make('role')
->options(Role::class)
->required(),
]);
}
}
The form() and infolist() overrides still exist for cases where you need divergent layouts, but the default resolution delegates to schema(). If you only override schema(), Filament renders form inputs on edit pages and text entries on view pages automatically.
Reusable Schema Components
The real power emerges when you extract shared layouts into dedicated classes:
namespace App\Filament\Schemas;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\DateTimePicker;
class AuditSchema
{
public static function components(): array
{
return [
Section::make('Audit')
->collapsed()
->schema([
TextInput::make('created_by')->disabled(),
DateTimePicker::make('created_at')->disabled(),
DateTimePicker::make('updated_at')->disabled(),
]),
];
}
}
Then compose it anywhere:
public static function schema(Schema $schema): Schema
{
return $schema->components([
// ... resource-specific fields
...AuditSchema::components(),
]);
}
This pattern replaces the v3 habit of duplicating Section blocks across form() and infolist() with slightly different entry types.
Conditional Rendering Without Duplication
A common v3 pain point was toggling visibility differently between form and infolist. In v4 you can inspect the schema's context:
use Filament\Schemas\Schema;
use Filament\Forms\Components\Textarea;
Textarea::make('notes')
->visible(fn (Schema $livewire) => ! $livewire->isReadOnly()),
The isReadOnly() helper returns true when the schema is rendering as an infolist, letting you hide fields that make no sense in a read context without maintaining two trees.
Gotchas to Watch For
Validation rules still live on form components. When a TextInput renders as a text entry, its ->required() and ->rules() calls are silently ignored — they don't bleed into infolist rendering. This is correct behaviour, but it means you should not rely on schema-level validation for display logic.
Custom entry types need explicit registration. If you built a custom Infolist\Components\MoneyEntry in v3, it won't automatically map from a MoneyInput form component. You must either extend the new Component base class or keep the explicit infolist() override for that resource.
Livewire state keys are unchanged. The schema API is a rendering abstraction; the underlying Livewire component state and $data array behave identically to v3.
Key Takeaways
- Define
schema()once; Filament resolves form vs. infolist rendering automatically. - Extract shared layout blocks into plain PHP classes returning
array— no base class needed. - Use
isReadOnly()for context-aware visibility instead of duplicating components. - Custom v3 entry types require explicit porting; they don't auto-map from form components.
form()andinfolist()overrides remain valid escape hatches for genuinely divergent layouts.