> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dottxt.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# UI Generation

> Generate deterministic UI specs from natural language using JSON Schema as the rendering contract.

Generating UI from natural language is powerful but fragile. If the model outputs a component name your renderer doesn't support, a field name with spaces, or 50 form fields for a simple request, your frontend either crashes or renders garbage. A strict schema turns the model's output into a guaranteed-renderable form spec: known component types, valid field names, bounded counts.

The key insight is that your schema should mirror what your renderer actually accepts. If your React form builder supports six component types, the schema's `component` enum should list exactly those six. If your layout engine supports single and two-column modes, the schema should enumerate those. The model generates within these bounds, and the output is always renderable.

## Goal

Convert product requests into a form specification that a React frontend can render directly without validation or transformation.

## Recommended contract

<CodeGroup>
  ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  {
    "type": "object",
    "properties": {
      "title": { "type": "string", "minLength": 3, "maxLength": 80 },
      "layout": { "type": "string", "enum": ["single_column", "two_column"] },
      "fields": {
        "type": "array",
        "minItems": 1,
        "maxItems": 20,
        "items": {
          "type": "object",
          "properties": {
            "name": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" },
            "label": { "type": "string", "minLength": 1, "maxLength": 50 },
            "component": {
              "type": "string",
              "enum": ["text", "email", "number", "select", "textarea", "checkbox"]
            },
            "required": { "type": "boolean" },
            "placeholder": { "type": "string", "maxLength": 80 },
            "options": {
              "type": "array",
              "items": { "type": "string", "minLength": 1, "maxLength": 40 },
              "maxItems": 20
            }
          },
          "required": ["name", "label", "component", "required"],
          "additionalProperties": false,
          "allOf": [
            {
              "if": { "properties": { "component": { "const": "select" } } },
              "then": { "required": ["options"] }
            }
          ]
        }
      }
    },
    "required": ["title", "layout", "fields"],
    "additionalProperties": false
  }
  ```

  ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  from typing import Annotated, Literal
  from pydantic import BaseModel, ConfigDict, Field

  OptionLabel = Annotated[str, Field(min_length=1, max_length=40)]

  class BaseField(BaseModel):
      model_config = ConfigDict(extra="forbid")
      name: str = Field(..., pattern=r"^[a-z][a-z0-9_]*$")
      label: str = Field(..., min_length=1, max_length=50)
      required: bool
      placeholder: str | None = Field(None, max_length=80)

  class SelectField(BaseField):
      component: Literal["select"]
      options: list[OptionLabel] = Field(..., max_length=20)

  class SimpleField(BaseField):
      component: Literal["text", "email", "number", "textarea", "checkbox"]

  FieldSpec = Annotated[SelectField | SimpleField, Field(discriminator="component")]

  class FormSpec(BaseModel):
      model_config = ConfigDict(extra="forbid")
      title: str = Field(..., min_length=3, max_length=80)
      layout: Literal["single_column", "two_column"]
      fields: list[FieldSpec] = Field(..., min_length=1, max_length=20)
  ```

  ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  import { z } from "zod";

  const baseFieldSchema = z.object({
    name: z.string().regex(/^[a-z][a-z0-9_]*$/),
    label: z.string().min(1).max(50),
    required: z.boolean(),
    placeholder: z.string().max(80).optional(),
  });

  const formFieldSchema = z.discriminatedUnion("component", [
    baseFieldSchema.extend({
      component: z.literal("select"),
      options: z.array(z.string().min(1).max(40)).max(20),
    }).strict(),
    baseFieldSchema.extend({ component: z.literal("text") }).strict(),
    baseFieldSchema.extend({ component: z.literal("email") }).strict(),
    baseFieldSchema.extend({ component: z.literal("number") }).strict(),
    baseFieldSchema.extend({ component: z.literal("textarea") }).strict(),
    baseFieldSchema.extend({ component: z.literal("checkbox") }).strict(),
  ]);

  const formSpecSchema = z.object({
    title: z.string().min(3).max(80),
    layout: z.enum(["single_column", "two_column"]),
    fields: z.array(formFieldSchema).min(1).max(20),
  }).strict();
  ```

  ```bash curl theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  curl https://api.dottxt.ai/v1/chat/completions \
    -H "Authorization: Bearer $DOTTXT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "model": "openai/gpt-oss-20b",
      "messages": [{ "role": "user", "content": "Build a lead capture form for B2B demo requests. Need company email, team size with options 1-10, 11-50, 51-200, and 201+, and whether they want migration help." }],
      "response_format": {
        "type": "json_schema",
        "json_schema": {
          "name": "form_spec",
          "schema": {
            "type": "object",
            "properties": {
              "title": { "type": "string", "minLength": 3, "maxLength": 80 },
              "layout": { "type": "string", "enum": ["single_column", "two_column"] },
              "fields": {
                "type": "array",
                "minItems": 1,
                "maxItems": 20,
                "items": {
                  "type": "object",
                  "properties": {
                    "name": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" },
                    "label": { "type": "string", "minLength": 1, "maxLength": 50 },
                    "component": { "type": "string", "enum": ["text", "email", "number", "select", "textarea", "checkbox"] },
                    "required": { "type": "boolean" },
                    "placeholder": { "type": "string", "maxLength": 80 },
                    "options": { "type": "array", "items": { "type": "string", "minLength": 1, "maxLength": 40 }, "maxItems": 20 }
                  },
                  "required": ["name", "label", "component", "required"],
                  "additionalProperties": false,
                  "allOf": [
                    {
                      "if": { "properties": { "component": { "const": "select" } } },
                      "then": { "required": ["options"] }
                    }
                  ]
                }
              }
            },
            "required": ["title", "layout", "fields"],
            "additionalProperties": false
          }
        }
      }
    }'
  ```
</CodeGroup>

## Prompt pattern

```text theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
You generate UI form specs for a React renderer.
Return JSON only and follow the schema exactly.
Keep labels concise and names snake_case.
Use select only when options are explicit in the request.
```

## Example input

```text theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
Build a lead capture form for B2B demo requests.
Need company email, team size with options 1-10, 11-50, 51-200, and 201+, and whether they want migration help.
```

## Example output

```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{
  "title": "B2B Demo Request",
  "layout": "two_column",
  "fields": [
    {
      "name": "company_email",
      "label": "Company Email",
      "component": "email",
      "required": true,
      "placeholder": "name@company.com"
    },
    {
      "name": "team_size",
      "label": "Team Size",
      "component": "select",
      "required": true,
      "options": ["1-10", "11-50", "51-200", "201+"]
    },
    {
      "name": "needs_migration_help",
      "label": "Need Migration Help",
      "component": "checkbox",
      "required": false
    }
  ]
}
```

## Implementation tips

* **Component enum mirrors your renderer.** If your form builder gains a new component type (e.g., `date_picker`), add it to the schema enum. If the model suggests a type not in the enum, the schema rejects it at generation time.
* **Conditional requirements prevent incomplete specs.** The `allOf` block requires `options` when `component` is `"select"`. Without this, the model might produce a select field with no options: valid JSON, but unrenderable.
* **Bounds protect layout.** `maxItems: 20` on fields prevents the model from generating overwhelming forms. `maxLength` on labels and placeholders keeps text from breaking your CSS grid.
* **Field name pattern keeps keys predictable.** The `"^[a-z][a-z0-9_]*$"` pattern on `name` enforces lowercase snake\_case field keys, which keeps form state and backend mappings consistent without extra normalization.

## Related docs

* [Conditional requirements](/json-schema/conditional-requirements): require `options` only when `component` is `"select"`, and similar patterns
* [Unions of objects](/json-schema/union-of-objects): discriminated unions for different component types
* [Object reference](/json-schema/reference/object) | [Conditionals reference](/json-schema/reference/conditionals) | [String reference](/json-schema/reference/string)
