webdevcraft/liform-bundle
Symfony bundle that turns JSON Schema into native Symfony forms via LiForm. Define form structure in JSON Schema and let the bundle build, render, and validate forms automatically, reducing boilerplate for schema-driven or API-defined forms.
Install
composer require webdevcraft/liform-bundle
The bundle wraps liform/liform, which converts a built Symfony\Component\Form\FormInterface into a JSON Schema document. It is not a Flex recipe, so you must register it manually in config/bundles.php:
WebdevCraft\LiformBundle\LiformBundle::class => ['all' => true],
Where to look first
Resources/config/services.xml (or config/services.php) in the bundle — confirm the service ID it registers for Liform\Resolver (usually public as liform.resolver). Everything else hinges on this one service.liform/liform README and the Liform\Transformer\* classes — that is where 90% of behaviour lives; the bundle is essentially DI glue.DependencyInjection/*Extension.php — check what (if anything) is configurable and whether it references legacy %...class% parameters.First use case
You have a Symfony form (say RegistrationType) and a JS SPA that should render it dynamically. Expose the schema:
public function schema(FormFactoryInterface $forms, Resolver $resolver): JsonResponse
{
$form = $forms->create(RegistrationType::class); // build, do NOT handleRequest()
return $this->json($resolver->resolve($form));
}
Fetch /api/schema/registration and feed the result to a JSON-Schema form renderer (react-jsonschema-form, vue-form-json-schema, @jsonforms/core). Submission still goes to your normal Symfony form endpoint — Liform only replaces the rendering half of the loop.
One schema endpoint, keyed by form alias. Avoid a controller per form. Keep a whitelist map of alias => FormType::class and resolve by alias; this also gives you one place to enforce authorization and caching.
Treat the form type as the single source of truth. Constraints (NotBlank, Length, Regex, Choice) are what make the schema useful for client-side validation. Never hand-write validation rules in JS that duplicate constraints — if the schema can't express something, add a FormTypeExtension.
Symfony form type extensions as the schema metadata layer. Help text becomes description, attr becomes custom keywords, label becomes title. This is the cleanest place for annotation work because it applies everywhere the form is used:
$builder->add('email', null, [
'help' => 'We only use this for receipts.', // -> description
'attr' => ['placeholder' => '[email protected]'],
]);
Cache the output. A schema is deterministic for a given form definition + locale (labels are translated at resolve time). Cache with a key of form alias + locale + build hash; a deploy that changes a form type naturally busts the hash. Resolving dozens of forms on every request is measurable.
Snapshot-test the schemas. In a functional test, resolve each whitelisted form and compare against a committed .json fixture. This is the cheapest guard against a developer silently changing a public API contract by adding/renaming a form field.
Custom transformers when a field type misbehaves. Liform\Resolver accepts transformers with priorities; reach for one when (e.g.) a custom DataType needs a proper type/format instead of falling back to a permissive {}.
services.yaml replicating liform.resolver buys you upgrade freedom and a supported dependency.symfony/form. Expect --ignore-platform-reqs or a fork/repositories vcs entry; verify the form API it uses hasn't shifted.%form.factory.class%-style arguments — those parameters were removed in Symfony 5.x and will fatal at container compile, not at runtime.$resolver->resolve() on a form that's been submitted/validated produces noise (validation state, submitted data). Build it and resolve it; the schema is about structure, not values.properties. _token and your SubmitType field usually show up. Strip them in a post-processing step or via a transformer; clients shouldn't see _token.CollectionType::allow_add yields array schemas whose items and additionalProperties semantics are easy to get wrong, and prototype fields can surface as 0, 1. Always eyeball a collection schema before shipping.DataTransformerInterface (entity ID ↔ object, money strings) is not modelled — the schema describes the underlying type. Document mismatches; don't assume the schema describes what the user actually types.choice_loader-backed ChoiceType may produce an empty or absent enum because resolving would hit the database. Prefer static choices or a separate /options endpoint for large lists.$ref/descriptions. Put the endpoint behind the same firewall as the form.null or empty properties almost always means the form type wasn't recognized or a custom type lacks a transformer. Confirm the resolver actually received the bundle's autowired instance and not a fresh, unconfigured one.How can I help you explore Laravel packages today?