← Назад к вопросам
Как реализовать генерацию и рендеринг форм на основе модели, аналогично Form-компоненту в Django?
3.0 Senior🔥 91 комментариев
#Django#Архитектура и паттерны
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Генерация и рендеринг форм на основе модели
Аналогично Django Forms, можно создать систему для автоматического генерирования и рендеринга форм на основе моделей данных. Рассмотрю несколько подходов.
1. Вариант 1: Pydantic + HTMLGen (простой)
Используем Pydantic для определения моделей и генерируем HTML автоматически:
from pydantic import BaseModel, Field, EmailStr, field_validator
from typing import Optional, Type
from html import escape
class FormField:
"""Мета-информация для формы"""
def __init__(self, field_type, label=None, required=True,
placeholder=None, help_text=None, choices=None):
self.field_type = field_type
self.label = label
self.required = required
self.placeholder = placeholder
self.help_text = help_text
self.choices = choices
class UserRegistration(BaseModel):
username: str = Field(..., min_length=3, max_length=50)
email: EmailStr
password: str = Field(..., min_length=8)
confirm_password: str
age: Optional[int] = Field(None, ge=18, le=120)
country: str = Field(default='US')
subscribe: bool = Field(default=False)
@field_validator('confirm_password')
def passwords_match(cls, v, info):
if v != info.data.get('password'):
raise ValueError('Пароли не совпадают')
return v
class FormRenderer:
"""Генерирует HTML форму из Pydantic модели"""
FIELD_TYPES = {
str: 'text',
int: 'number',
bool: 'checkbox',
EmailStr: 'email',
float: 'number',
}
def __init__(self, model: Type[BaseModel]):
self.model = model
self.fields = model.model_fields
def render_field(self, field_name: str, field_info) -> str:
"""Генерирует HTML для одного поля"""
field_type = self.FIELD_TYPES.get(
field_info.annotation, 'text'
)
required = field_info.is_required()
label = field_info.title or field_name.replace('_', ' ').title()
if field_type == 'checkbox':
return f"""
<div class="form-group">
<label>
<input type="checkbox" name="{field_name}" />
{escape(label)}
</label>
</div>
"""
else:
required_attr = 'required' if required else ''
return f"""
<div class="form-group">
<label for="{field_name}">{escape(label)}</label>
<input
type="{field_type}"
id="{field_name}"
name="{field_name}"
{required_attr}
/>
</div>
"""
def render(self, action: str = '/submit', method: str = 'POST') -> str:
"""Генерирует полную форму"""
fields_html = ''.join(
self.render_field(name, field)
for name, field in self.fields.items()
)
return f"""
<form action="{action}" method="{method}" class="form">
{fields_html}
<button type="submit" class="btn btn-primary">Submit</button>
</form>
"""
# Использование
renderer = FormRenderer(UserRegistration)
html = renderer.render('/api/register')
print(html)
2. Вариант 2: Классический Django-подобный подход
Создаём систему похожую на Django Forms:
from typing import Any, Dict, List, Type
from dataclasses import dataclass, fields as dc_fields
from abc import ABC, abstractmethod
@dataclass
class Field(ABC):
"""Базовое поле формы"""
name: str
label: str = None
required: bool = True
help_text: str = None
default: Any = None
def __post_init__(self):
if self.label is None:
self.label = self.name.replace('_', ' ').title()
@abstractmethod
def render(self) -> str:
pass
@abstractmethod
def validate(self, value: Any) -> Any:
pass
@dataclass
class StringField(Field):
min_length: int = 0
max_length: int = 255
placeholder: str = None
def render(self) -> str:
required = 'required' if self.required else ''
placeholder = f'placeholder="{self.placeholder}"' if self.placeholder else ''
return f"""
<div class="form-group">
<label for="{self.name}">{self.label}</label>
<input
type="text"
id="{self.name}"
name="{self.name}"
minlength="{self.min_length}"
maxlength="{self.max_length}"
{placeholder}
{required}
/>
{f'<small>{self.help_text}</small>' if self.help_text else ''}
</div>
"""
def validate(self, value: str) -> str:
if not value and self.required:
raise ValueError(f'{self.label} обязательно')
if value and (len(value) < self.min_length or len(value) > self.max_length):
raise ValueError(f'{self.label} должно быть от {self.min_length} до {self.max_length} символов')
return value
@dataclass
class IntegerField(Field):
min_value: int = None
max_value: int = None
def render(self) -> str:
required = 'required' if self.required else ''
min_attr = f'min="{self.min_value}"' if self.min_value is not None else ''
max_attr = f'max="{self.max_value}"' if self.max_value is not None else ''
return f"""
<div class="form-group">
<label for="{self.name}">{self.label}</label>
<input
type="number"
id="{self.name}"
name="{self.name}"
{min_attr}
{max_attr}
{required}
/>
</div>
"""
def validate(self, value: int) -> int:
if value is None and self.required:
raise ValueError(f'{self.label} обязательно')
if value is not None:
if self.min_value is not None and value < self.min_value:
raise ValueError(f'{self.label} не может быть меньше {self.min_value}')
if self.max_value is not None and value > self.max_value:
raise ValueError(f'{self.label} не может быть больше {self.max_value}')
return value
@dataclass
class BooleanField(Field):
def render(self) -> str:
checked = 'checked' if self.default else ''
return f"""
<div class="form-group">
<label>
<input
type="checkbox"
name="{self.name}"
{checked}
/>
{self.label}
</label>
</div>
"""
def validate(self, value: bool) -> bool:
return bool(value)
class Form:
"""Контейнер для полей формы"""
def __init__(self):
self.fields: Dict[str, Field] = {}
self.errors: Dict[str, List[str]] = {}
def add_field(self, field: Field) -> None:
self.fields[field.name] = field
def render(self, action: str = '/submit', method: str = 'POST') -> str:
fields_html = '\n'.join(
field.render() for field in self.fields.values()
)
return f"""
<form action="{action}" method="{method}" class="form">
{fields_html}
<button type="submit" class="btn btn-primary">Submit</button>
</form>
"""
def validate(self, data: Dict[str, Any]) -> bool:
"""Валидирует все поля"""
self.errors.clear()
for field_name, field in self.fields.items():
try:
value = data.get(field_name)
field.validate(value)
except ValueError as e:
if field_name not in self.errors:
self.errors[field_name] = []
self.errors[field_name].append(str(e))
return len(self.errors) == 0
# Использование
class UserForm(Form):
def __init__(self):
super().__init__()
self.add_field(StringField(
'username',
label='Username',
min_length=3,
max_length=50,
placeholder='Enter username'
))
self.add_field(StringField(
'email',
label='Email',
placeholder='user@example.com'
))
self.add_field(IntegerField(
'age',
label='Age',
min_value=18,
max_value=120,
required=False
))
self.add_field(BooleanField(
'subscribe',
label='Subscribe to newsletter',
default=False,
required=False
))
form = UserForm()
print(form.render('/api/register'))
# Валидация
data = {'username': 'john', 'email': 'john@example.com', 'age': 25, 'subscribe': True}
if form.validate(data):
print('Форма валидна')
else:
print('Ошибки:', form.errors)
3. Вариант 3: FastAPI + SQLAlchemy (production)
Для реальных приложений используй готовые инструменты:
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from sqlalchemy import Column, String, Integer, Boolean, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import Session
from pydantic import BaseModel, EmailStr, validator
app = FastAPI()
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
username = Column(String(50), unique=True)
email = Column(String(255), unique=True)
age = Column(Integer)
is_active = Column(Boolean, default=True)
class UserSchema(BaseModel):
username: str
email: EmailStr
age: int
is_active: bool = True
class Config:
from_attributes = True
# Автоматическое преобразование модели в HTML форму
def generate_form_html(schema_class, action: str) -> str:
"""Генерирует HTML форму из Pydantic модели"""
fields_html = []
for field_name, field_info in schema_class.model_fields.items():
field_type = field_info.annotation
required = field_info.is_required()
if field_type == bool:
fields_html.append(f"""
<label>
<input type="checkbox" name="{field_name}" />
{field_name.title()}
</label>
""")
elif field_type == int:
fields_html.append(f"""
<div>
<label for="{field_name}">{field_name.title()}</label>
<input type="number" id="{field_name}" name="{field_name}" {'required' if required else ''} />
</div>
""")
else: # str, EmailStr
input_type = 'email' if field_type == EmailStr else 'text'
fields_html.append(f"""
<div>
<label for="{field_name}">{field_name.title()}</label>
<input type="{input_type}" id="{field_name}" name="{field_name}" {'required' if required else ''} />
</div>
""")
return f"""
<html>
<body>
<form action="{action}" method="POST">
{''.join(fields_html)}
<button type="submit">Submit</button>
</form>
</body>
</html>
"""
@app.get('/register', response_class=HTMLResponse)
async def register_form():
return generate_form_html(UserSchema, '/api/users')
@app.post('/api/users')
async def create_user(user: UserSchema):
return {'message': 'User created', 'user': user}
4. Использование готовых библиотек
В продакшене используй проверенные решения:
# WTForms — классический выбор
pip install wtforms
# FastAPI + SQLAdmin — автоматический админ-панель
pip install sqladmin
# Pydantic validation
pip install pydantic
from wtforms import Form, StringField, IntegerField, BooleanField, validators
from wtforms.validators import DataRequired, Email, Length
class UserForm(Form):
username = StringField('Username', [
validators.DataRequired(),
validators.Length(min=3, max=50)
])
email = StringField('Email', [
validators.DataRequired(),
validators.Email()
])
age = IntegerField('Age', [
validators.NumberRange(min=18, max=120)
])
subscribe = BooleanField('Subscribe')
Резюме
Для генерации форм:
- Простой вариант — Pydantic + HTMLGen (для прототипов)
- Средний — собственная система Field + Form (для контроля)
- Production — FastAPI + SQLAlchemy + SQLAdmin или WTForms
- Фронтенд-генерация — JSON schema → React/Vue компоненты