I recently ran into an issue at work. We have an engineering analysis process that required large amount of inputs to be collected from engineers which contained deeply nested structure with multiple input types. As any sane python developer will do these days, we used pydantic models to both structure our mental model and also to validate this massive input (which comes in the form of a yaml file)
Even though yaml is a decent format to use for structured input, it's still quite verbose to hand-craft and it can be quite tricky to get all the indents right. So we needed a decent UI which can be used to fill in this input. The thought was, Instead of writing a UI specifically for our data structure, why not infer the structure dynamically from the already existing pydantic model? It can also give us proper (and upfront) validation errors if there are errors in the input. If we can sprinkle a little more customization on top of the existing pydantic model, it can be quite powerful. That's exactly what I ended up doing (with a lot of help from claude opus 4.5). We thought of open-sourcing our solution.
Here's pydantic-ui. It a bit opinionated, but also lets you configure quite a lot. Please do test it out and any feedback is welcome!
Example usage
from fastapi import FastAPI
from pydantic import BaseModel, Field
from pydantic_ui import DisplayConfig, FieldConfig, Renderer, UIConfig, create_pydantic_ui
# Define your Pydantic model
class Address(BaseModel):
street: str
city: str
zipcode: str
class Person(BaseModel):
name: str = Field(min_length=1, max_length=100)
age: int = Field(gt=0, lt=150)
email: str
address: Address = Field(description="Permanent address")
tags: list[str] = []
# Create FastAPI app and mount pydantic-ui
app = FastAPI()
app.include_router(
create_pydantic_ui(
Person,
prefix="/editor",
ui_config=UIConfig(
title="Person Editor",
show_save_reset=True,
attr_configs={
"age": FieldConfig(
renderer=Renderer.SLIDER,
display=DisplayConfig(
title="User Age",
)
)
},
),
),
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
This will be rendered like below.


Top comments (0)