You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[](https://docs.pydantic.dev/)
12
+
13
+
</div>
14
+
15
+
---
16
+
17
+
## Why codeforms?
18
+
19
+
Traditional form libraries (Django Forms, WTForms) define forms as **Python classes**. A new form — or a change to an existing one — means a code change, a review and a deploy.
20
+
21
+
codeforms defines forms as **data**: a Pydantic model that round-trips losslessly to JSON. The same definition can be stored in a database, edited by a non-developer in a form builder, versioned per tenant, and shipped at runtime — while still giving you strict server-side validation and real Pydantic models.
│ of truth) │ ──────▶ Pydantic model (server-side)
29
+
Form builder ────▶ │ │ ──────▶ JSON / dict
30
+
└──────────────────────┘
31
+
```
32
+
33
+
### What you get
34
+
35
+
|||
36
+
|---|---|
37
+
| 🗃️ **Forms as data**| Full JSON round-trip. Store definitions in a DB, version them, ship them at runtime — no redeploy to change a form. |
38
+
| 🛡️ **One source of truth**| The same definition drives server validation, HTML rendering, JSON Schema and client-side JS checks. They cannot drift apart. |
39
+
| ⚡ **Pydantic v2 native**|`FormDataValidator.create_model()` builds a real Pydantic model — drops straight into FastAPI, Flask, Litestar or any typed backend. |
40
+
| 🔀 **Dynamic logic built in**| Conditional visibility (`visible_when`), dependent options and multi-step wizards — declarative, serializable, and opt-in. |
41
+
| 🌍 **i18n out of the box**| English and Spanish included; register any locale at runtime. Validation messages follow the active locale. |
42
+
| 🧩 **Extensible by design**| Register your own field types with `register_field_type()` — no core changes, full serialization support. |
43
+
| 🔗 **Interoperable**| JSON Schema draft-07 export works with [React JSON Schema Form](https://github.com/rjsf-team/react-jsonschema-form), [Angular Formly](https://formly.dev/) and any JSON Schema validator. |
44
+
| 🪶 **Tiny footprint**| One dependency (`pydantic[email]`). Python 3.9+. MIT. 248 tests in CI. |
45
+
46
+
### Where it fits
47
+
48
+
-**Low-code / form builders** — persist user-designed forms as JSON and validate submissions safely.
49
+
-**BPM & workflow engines** — dynamic task forms (approvals, intake, review steps) defined per process, in the spirit of [Camunda form-js](https://bpmn.io/toolkit/form-js/).
50
+
-**Multi-tenant SaaS** — each customer gets a different form without a branch, a build or a deploy.
51
+
-**Surveys, intake and onboarding flows** — multi-step wizards with conditional branching.
52
+
-**Headless APIs** — serve `json_schema` to a React/Vue frontend and validate the same contract server-side.
53
+
54
+
### Compared to
55
+
56
+
|| codeforms | Django Forms / WTForms | Pydantic alone | React JSON Schema Form |
user = Model.model_validate({"full_name": "Ada", "email": "ada@example.com", "age": 36})
107
+
```
108
+
109
+
### 3. Render it
110
+
111
+
```python
112
+
html = form.export("html_bootstrap5", submit=True)["output"]
113
+
schema = form.export("json_schema")["output"]
114
+
```
115
+
116
+
### 4. Store it and bring it back
117
+
118
+
```python
119
+
raw = form.to_json() # persist anywhere: DB, S3, a config repo
120
+
form = Form.loads(raw) # lossless round-trip, including custom field types
121
+
```
122
+
123
+
That last step is the whole point: the form is data, not code.
124
+
38
125
### The `Form` Class
39
126
40
127
The `Form` class is the main container for your form structure.
41
128
42
129
-`id` — Auto-generated UUID.
43
130
-`name` — Form name (used in HTML export and validation).
44
-
-`fields` — A list of field objects (e.g. `TextField`, `EmailField`).
131
+
-`fields` — A list of field objects (e.g. `TextField`, `EmailField`). Always returns a **flat** list, even for grouped or stepped forms.
132
+
-`content` — The structured list of fields, `FieldGroup`s and `FormStep`s.
45
133
-`css_classes` — Optional CSS classes for the `<form>` tag.
46
134
-`version` — Form version number.
47
135
-`attributes` — Dictionary of additional HTML attributes for the `<form>` tag.
48
136
137
+
> **Backward compatibility:**`Form` accepts both `fields=` and `content=`. Legacy payloads load unchanged, and `.fields` always gives you the flat view.
138
+
49
139
## Field Types
50
140
51
141
All fields inherit from `FormFieldBase` and share these common attributes:
@@ -60,6 +150,7 @@ All fields inherit from `FormFieldBase` and share these common attributes:
60
150
-`css_classes` — CSS classes for the field element.
61
151
-`readonly` — Whether the field is read-only.
62
152
-`attributes` — Additional HTML attributes for the `<input>` tag.
153
+
-`visible_when` — Optional list of `VisibilityRule` (see [Conditional Visibility](#conditional-visibility)).
63
154
64
155
### Available Fields
65
156
@@ -76,6 +167,7 @@ All fields inherit from `FormFieldBase` and share these common attributes:
76
167
-`options`: List of `SelectOption(value="...", label="...")`.
-**`TextareaField`** — Multi-line text (`<textarea>`).
185
+
-`rows`, `minlength`, `maxlength`.
90
186
-**`ListField`** — Array of primitive values.
91
187
-`item_type`: Primitive type for each item (`text`, `number`, `email`, `url`, `date`).
92
188
-`min_items`, `max_items`: List size limits.
93
189
-**`ObjectListField`** — Array of homogeneous objects validated against nested subfields.
94
190
-`fields`: List of subfields that define each object shape.
95
191
-`min_items`, `max_items`: List size limits.
96
192
193
+
### Containers
194
+
195
+
-**`FieldGroup`** — Groups fields into a titled section (`<fieldset>` / `<legend>` in HTML export).
196
+
-`title`, `description`, `fields`.
197
+
-`collapsible`, `collapsed`: Collapsible section metadata for your frontend.
198
+
-**`FormStep`** — A wizard step; see [Multi-Step Wizard Forms](#multi-step-wizard-forms).
199
+
200
+
Fields inside containers are always reachable through the flat `form.fields` view.
201
+
97
202
### `ObjectListField`
98
203
99
204
Use `ObjectListField` when a form needs a repeatable list of structured rows, such as parallel approvers, attendees with roles, or line items.
@@ -234,7 +339,7 @@ print(dict_output)
234
339
|`json`| JSON representation of the form |
235
340
|`dict`| Python dictionary representation |
236
341
237
-
HTML export can also generate a `<script>` block for basic client-side validation.
342
+
HTML export can also generate a `<script>` block for basic client-side validation, derived from the same field constraints used on the server — so the two can't drift apart. All rendered values are HTML-escaped.
238
343
239
344
### JSON Schema Export
240
345
@@ -444,6 +549,8 @@ See [`examples/i18n_usage.py`](examples/i18n_usage.py) for a full working exampl
444
549
445
550
## Dynamic Forms
446
551
552
+
All dynamic behavior below is **opt-in and additive**: forms without dynamic metadata behave exactly as before, and every new JSON key is optional.
553
+
447
554
### Conditional Visibility
448
555
449
556
Fields can be shown or hidden based on the value of other fields using `visible_when`. This is metadata that your frontend can use for dynamic UI, and the backend can respect during validation.
@@ -657,6 +764,63 @@ for name, classes in sorted(get_registered_field_types().items()):
657
764
658
765
See [`examples/custom_fields.py`](examples/custom_fields.py) for a full working example.
659
766
767
+
---
768
+
769
+
## Compatibility Guarantees
770
+
771
+
codeforms takes backward compatibility seriously:
772
+
773
+
- Existing usage — `Form(fields=[...])`, `form.fields`, `validate_data()`, `validate_form_data()`, current HTML output — keeps working without code changes.
774
+
- New schema keys are **additive and optional**; previously serialized forms load unchanged.
775
+
- Dynamic behavior (visibility, dependent options, wizards) is **disabled by default** and enabled explicitly.
776
+
- Legacy paths get a deprecation warning for at least one minor release before removal in a major release.
777
+
778
+
## Examples
779
+
780
+
| Example | Shows |
781
+
|---|---|
782
+
|[`basic_usage.py`](examples/basic_usage.py)| Defining, validating and exporting a form |
783
+
|[`conditional_visibility.py`](examples/conditional_visibility.py)|`visible_when` rules and dynamic validation |
784
+
|[`dependent_options.py`](examples/dependent_options.py)| Option sets driven by another field |
785
+
|[`wizard_form.py`](examples/wizard_form.py)| Multi-step forms and per-step validation |
786
+
|[`custom_fields.py`](examples/custom_fields.py)| Registering your own field types |
787
+
|[`i18n_usage.py`](examples/i18n_usage.py)| Locales and custom translations |
0 commit comments