Settings with a Code Field¶
A settings form where part of the settings is JSON, TOML or a script, typed into a text area with syntax highlighting and a Format button, and checked on the server.
models.py
import json
import tomllib
from typing import Any
from pydantic import field_validator
from pydantic_schemaforms import Field, FormModel
class ServiceSettings(FormModel):
name: str = Field(title='Service name', pattern=r'^[a-z][a-z0-9-]*$')
config: str = Field(
'{\n "replicas": 2\n}',
title='Config (JSON)',
ui_element='textarea',
ui_options={'language': 'json', 'rows': 10},
)
build: str = Field(
'[tool.build]\ntarget = "release"\n',
title='Build settings (TOML)',
ui_element='textarea',
ui_options={'language': 'toml', 'rows': 6},
)
deploy_script: str = Field(
'',
title='Deploy script',
ui_element='textarea',
ui_options={'language': 'bash', 'code_highlight': False},
)
@field_validator('config')
@classmethod
def valid_json_object(cls, value: str) -> str:
try:
parsed: Any = json.loads(value)
except json.JSONDecodeError as exc:
raise ValueError(f'Not valid JSON: {exc.msg} (line {exc.lineno})') from None
if not isinstance(parsed, dict):
raise ValueError('The config must be a JSON object')
return value
@field_validator('build')
@classmethod
def valid_toml(cls, value: str) -> str:
try:
tomllib.loads(value)
except tomllib.TOMLDecodeError as exc:
raise ValueError(f'Not valid TOML: {exc}') from None
return value
main.py
import json
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from models import ServiceSettings
from pydantic_schemaforms import html, render_form_html_async
app = FastAPI()
FORM_OPTIONS = {'submit_url': '/settings', 'include_framework_assets': True, 'asset_mode': 'cdn'}
@app.get('/settings', response_class=HTMLResponse)
async def show_settings():
return await render_form_html_async(ServiceSettings, **FORM_OPTIONS)
@app.post('/settings', response_class=HTMLResponse)
async def save_settings(request: Request):
result = ServiceSettings.validate(await request.form(), **FORM_OPTIONS)
if not result.is_valid:
return await result.render_with_errors_async()
replicas = json.loads(result.data['config']).get('replicas', 1)
return html(t'<p>Saved {result.data["name"]} with {replicas} replicas.</p>')
main.py
import json
from flask import Flask, request
from models import ServiceSettings
from pydantic_schemaforms import html, render_form_html
app = Flask(__name__)
FORM_OPTIONS = {'submit_url': '/settings', 'include_framework_assets': True, 'asset_mode': 'cdn'}
@app.get('/settings')
def show_settings():
return render_form_html(ServiceSettings, **FORM_OPTIONS)
@app.post('/settings')
def save_settings():
result = ServiceSettings.validate(request.form, **FORM_OPTIONS)
if not result.is_valid:
return result.render_with_errors()
replicas = json.loads(result.data['config']).get('replicas', 1)
return html(t'<p>Saved {result.data["name"]} with {replicas} replicas.</p>')
How it works¶
ui_options={"language": ...}turns a text area into a small code editor: highlighting, Tab to indent, and a Format button. It's still a plain<textarea>, so it works without JavaScript.- The languages are
json,yaml,toml,bashandpython. JSON, YAML and TOML are really reformatted. Bash and Python only get their whitespace cleaned up, and the button says so. - The validators parse the text and turn a parse error into a message under the field. The browser's Format button already flags bad JSON, but only the server check counts.
code_highlight=False,code_format=Falseandcode_tab_indent=Falseturn off each part;indent_sizesets the indent (default 2).
The value stays a string. Parse it again where you use it, as the route does, or add a second field typed as a model if you want the parsed data validated field by field.