How Embabel Form Generation and Binding Works with FormGenerator
Embabel uses SimpleFormGenerator to reflectively inspect Kotlin or Java classes and build UI Form objects, then employs FormBinder implementations to map submitted values back to type-safe instances with automatic conversion and validation.
The embabel-agent repository provides a declarative framework for turning data classes into interactive forms without manual UI code. By combining reflection-based generation with annotation-driven binding, the system bridges the gap between backend models and frontend controls while maintaining compile-time safety.
Form Generation with SimpleFormGenerator
The generation phase transforms annotated classes into structured Form objects containing UI controls. According to the source code in embabel-agent-api, this process centers on the FormGenerator interface and its concrete implementation.
The FormGenerator Interface
In embabel-agent-api/src/main/kotlin/com/embabel/ux/form/FormGenerator.kt, the FormGenerator interface declares the contract for form creation:
interface FormGenerator {
fun <T : Any> generateForm(dataClass: KClass<T>, title: String): Form
}
The SimpleFormGenerator object in SimpleFormGenerator.kt provides the standard implementation, using Kotlin reflection to inspect class properties and Java fields.
Property Inspection and Control Creation
SimpleFormGenerator.getPropertiesInDeclarationOrder obtains properties in the same order as underlying Java fields, guaranteeing stable UI ordering. The generation follows this sequence:
- Collect properties – Reflective inspection gathers all class properties
- Filter fields – Removes properties annotated with
@NoFormFieldor non-nullable constructor parameters with default values (like auto-generated timestamps) - Create controls –
createControlForPropertyexamines each property's Kotlin type (String,Int,Boolean,LocalDate, etc.) and returns appropriate controls likeTextField,Checkbox, orDatePicker - Append submit button – Adds a final
Buttoncontrol automatically - Return Form – Packages controls with the supplied title into a
Forminstance
Annotation-Based Customization
Developers control generation through annotations defined in SimpleFormGenerator.kt:
@FormField– Overrides the default control ID and maps form fields to constructor parameters@NoFormField– Excludes properties from the generated form (useful for server-side auto-populated fields)@Text– Provides human-readable labels and UI hints for controls
Form Binding with FormBinder
After users submit forms, the binding phase reconstructs class instances from raw ControlValue objects. The FormBinder implementations in FormBinder.kt handle type conversion, validation, and object instantiation.
Java and Kotlin Binder Implementations
The FormBinder<T> interface defines bind(submissionResult): T, with two primary implementations:
JavaFormBinder– Handles plain Java classes and records inFormBinder.ktKotlinFormBinder– Handles Kotlin data classes, delegating toJavaFormBinderfor non-data classes
The extension helper bindTo in the same file provides a convenient one-liner for binding FormSubmissionResult to target types.
The Binding Flow
FormBinder.formBinder selects the appropriate implementation using Spring's KotlinDetector. The binding process then executes:
- Validation check – Throws
ValidationExceptionifFormSubmissionResult.validis false - Path selection – Routes to
KotlinFormBinderfor Kotlin types, otherwiseJavaFormBinder - Java binding – For records,
bindJavaRecordextracts components and invokes the canonical constructor; for plain classes,bindJavaConstructoriterates over constructors preferring those with more parameters - Kotlin binding –
bindKotlinDataClassobtains the primary constructor, matches properties to parameters viaconvertToParameterType, and callsconstructor.callBy - Instance creation – Returns fully populated, type-safe instances
Type Conversion
Both binders contain conversion utilities that translate UI-specific ControlValue objects to target types:
convertToJavaTypeandconvertJavaTextValuefor Java primitivesconvertTextValueandconvertNumberValuefor Kotlin types- Support for dates, times, collections, and file IDs through
ControlValuesubclasses likeTextValue,NumberValue, andBooleanValue
End-to-End Example
This complete example demonstrates the generation and binding cycle:
// Define the data model with form annotations
data class CreateUser(
@FormField("username") @Text(label = "User Name") val name: String,
@FormField("age") val age: Int,
@FormField("admin") val isAdmin: Boolean = false,
@NoFormField val createdAt: java.time.Instant = java.time.Instant.now()
)
// Generate the form
val form: Form = SimpleFormGenerator.generateForm(
dataClass = CreateUser::class,
title = "Create New User"
)
// Simulate form submission (normally from UI layer)
val submission = FormSubmissionResult(
valid = true,
values = mapOf(
"username" to ControlValue.TextValue("Alice"),
"age" to ControlValue.NumberValue(30.0),
"admin" to ControlValue.BooleanValue(true)
),
validationErrors = emptyMap()
)
// Bind back to Kotlin object
val user: CreateUser = submission.bindTo<CreateUser>()
println(user) // CreateUser(name=Alice, age=30, isAdmin=true, createdAt=...)
The @FormField annotations override default IDs for precise mapping, while @NoFormField keeps createdAt server-side only. The FormBinder automatically converts ControlValue objects to correct Kotlin types, respecting optional parameters and validation rules.
Summary
SimpleFormGeneratorinSimpleFormGenerator.ktreflects over classes to buildFormobjects with appropriate controls for each property typeFormBinderimplementations inFormBinder.ktreconstruct instances from submissions, with separate paths for Java records/classes and Kotlin data classes- Annotations (
@FormField,@NoFormField,@Text) provide declarative control over field visibility, IDs, and labels without boilerplate - Type conversion utilities handle primitives, dates, and complex objects automatically when binding
ControlValueto constructor parameters - Stable ordering is guaranteed by
getPropertiesInDeclarationOrder, which respects Java field declaration order
Frequently Asked Questions
How does Embabel handle nullable vs non-nullable fields in form generation?
SimpleFormGenerator inspects Kotlin type nullability when creating controls. Non-nullable constructor parameters with default values (such as timestamps or auto-generated IDs) are automatically excluded from the form via the filtering logic in getPropertiesInDeclarationOrder, while nullable fields remain optional in the UI.
Can FormGenerator handle Java Records as well as Kotlin data classes?
Yes. SimpleFormGenerator processes both Java Records and Kotlin data classes through reflection. During binding, JavaFormBinder.bindJavaRecord specifically handles record components by matching them to submitted form values and invoking the canonical constructor, while KotlinFormBinder handles Kotlin-specific features like default arguments via callBy.
What happens when form validation fails during binding?
If FormSubmissionResult.valid is false, the FormBinder throws a ValidationException containing the error map from validationErrors. This occurs before any type conversion or instantiation, ensuring that invalid submissions never attempt to create partial objects.
Where can I find test examples of form binding in the embabel-agent repository?
Unit tests demonstrating both Java and Kotlin binding behavior are located in embabel-agent-api/src/test/kotlin/com/embabel/ux/form/FormBinderTest.kt. For generation testing, see SimpleFormGeneratorTest.kt in the same directory, which validates control creation and property ordering.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →