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:

  1. Collect properties – Reflective inspection gathers all class properties
  2. Filter fields – Removes properties annotated with @NoFormField or non-nullable constructor parameters with default values (like auto-generated timestamps)
  3. Create controls – createControlForProperty examines each property's Kotlin type (String, Int, Boolean, LocalDate, etc.) and returns appropriate controls like TextField, Checkbox, or DatePicker
  4. Append submit button – Adds a final Button control automatically
  5. Return Form – Packages controls with the supplied title into a Form instance

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 in FormBinder.kt
  • KotlinFormBinder – Handles Kotlin data classes, delegating to JavaFormBinder for 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:

  1. Validation check – Throws ValidationException if FormSubmissionResult.valid is false
  2. Path selection – Routes to KotlinFormBinder for Kotlin types, otherwise JavaFormBinder
  3. Java binding – For records, bindJavaRecord extracts components and invokes the canonical constructor; for plain classes, bindJavaConstructor iterates over constructors preferring those with more parameters
  4. Kotlin binding – bindKotlinDataClass obtains the primary constructor, matches properties to parameters via convertToParameterType, and calls constructor.callBy
  5. Instance creation – Returns fully populated, type-safe instances

Type Conversion

Both binders contain conversion utilities that translate UI-specific ControlValue objects to target types:

  • convertToJavaType and convertJavaTextValue for Java primitives
  • convertTextValue and convertNumberValue for Kotlin types
  • Support for dates, times, collections, and file IDs through ControlValue subclasses like TextValue, NumberValue, and BooleanValue

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

  • SimpleFormGenerator in SimpleFormGenerator.kt reflects over classes to build Form objects with appropriate controls for each property type
  • FormBinder implementations in FormBinder.kt reconstruct 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 ControlValue to 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →