Skip to content

Bone overview

Compact reference for how every viur-core bone is declared as a viur-models field. The rule: the Python type decides the bone — never a string parameter. Everything pydantic and SQL already express (max_length, ge/le, optionality, defaults) is derived; only the ViUR-specific parts (descr, visible, params, …) go through Field. Optionality as usual via | None + default=None. All derivation rules in detail: bone reference; runnable example: deploy/models/example.py.

Full overview

Scalars

Bone Type string Field definition
StringBone str name: str = Field(descr="Name", max_length=100)
NumericBone (int) numeric rating: int \| None = Field(default=None, ge=1, le=5)
NumericBone (float/Decimal) numeric price: Decimal \| None = Field(default=None, decimal_places=2)
BooleanBone bool active: bool = Field(default=True)
DateBone date due: datetime \| None · day: date \| None · slot: time \| None

Selects

Bone Type string Field definition
SelectBone (Enum) select kind: EntryKind = Field(descr="Kind")
SelectBone (Literal) select status: Literal["new","done"] \| None = Field(values={…}, sa_type=String)
SelectCountryBone select.country country: Country \| None = Field(default=None)

String refinements (the type carries the bone)

Bone Type string Field definition
TextBone text message: Text = Field(default="", required=False)
EmailBone str.email mail: Email \| None = Field(default=None)
PhoneBone str.phone phone: Phone \| None = Field(default=None, max_length=15)
UriBone uri site: Uri \| None = Field(default=None)
ColorBone color tint: Color \| None = Field(default=None)
RawBone raw blob: Raw \| None = Field(default=None)
CodeBone/Jinja/Logics/Python raw.code template: Code \| None = Field(default=None)
JsonBone raw.json data: Json \| None = Field(default=None, sa_type=JSON)
UidBone uid uid: Uid \| None = Field(default=None) — ⚠️ value generated in a hook
SortIndexBone numeric.sortindex sortindex: SortIndex \| None = Field(default=None)
CredentialBone str.credential secret: Credential \| None = Field(default=None, visible=False) — write-only
PasswordBone password pwd: Password \| None = Field(default=None) — write-only, ⚠️ hashing in a hook
SpatialBone spatial pos: Spatial(bounds_lat=…, bounds_lng=…) \| None = Field(sa_type=JSON)

Multilingual

Bone Field definition
StringBone(languages=…) title: Language[str] = Field(languages=("de","en"), sa_type=JSON)
TextBone(languages=…) body: Language[Text] \| None = Field(default=None, sa_type=JSON)

The language list comes from languages= or project-wide from set_default_languages("de", "en"). Input is accepted dotted (title.de=…) and as a dict; the value is a {lang: value} dict.

Relations (SQL → SQL)

Bone Field definition
RelationalBone FK field (carries the bone parameters) + Relationship():
category_id: int \| None = Field(default=None, foreign_key="example_category.id", descr="Category")
category: ExampleCategory \| None = Relationship()
RelationalBone(multiple=True) tags: list[ExampleTag] = Relationship(link_model=EntryTagLink)
RelationalBone(using=RelSkel) Association object — the link table's payload columns ARE the using RelSkel:
class EntryTagLink(RelationLink, table=True): … tag: ExampleTag = Relationship(); weight: int = Field(ge=0, le=10)
tags: list[EntryTagLink] = Relationship(sa_relationship_kwargs={"cascade": "all, delete-orphan"})
RecordBone / AddressBone plain pydantic nesting: class Address(Record): …
address: Address \| None = Field(default=None, sa_type=RecordJSON(Address), format="$(street)")
multiple: stops: list[Address] = Field(default_factory=list, sa_type=RecordJSON(Address))

required is derived from the FK nullability; viur_ref_keys = ("name",) on the target decides relskel/dest. For multiple and using, cascade="all, delete-orphan" is mandatory.

Cross-store (SQL → datastore skeleton)

What is stored is the dest snapshot (key + ref_keys values) as JSON; the read on input rebuilds the snapshot and doubles as the existence check. Deleted targets do not block edits — the old snapshot stays. Changes to referenced skeletons propagate automatically: call viur.models.install_refresh_hooks() once at boot. It wraps the skeleton postSaved/postDeleted handlers and defers a targeted refresh_for_target over the viur_models_relations index table, the viur-relations equivalent; deletions default to set_null. For bulk repair there is refresh_crossstore(Model, missing=…) as a cron safety net.

Bone Field definition
UserBone owner: UserRef() \| None = Field(default=None, sa_type=JSON)
FileBone/ImageBone upload: FileRef() \| None = Field(default=None, sa_type=JSON) — a reference, not an upload
TreeLeafBone/TreeNodeBone node: SkeletonRef(kind, type_suffix="tree.node") \| None
any skeleton ref: SkeletonRef("feedback", ref_keys=("subject",)) \| None = Field(sa_type=JSON)
multiple (JSON array) refs: SkeletonRef("feedback", multiple=True) \| None = Field(sa_type=JSON)
multiple (link table) class FbLink(SkeletonLink, table=True): viur_kind = "feedback"; …
history: list[FbLink] = Relationship(sa_relationship_kwargs={"cascade": "all, delete-orphan"})

System and other

Bone Field definition
KeyBone / creationdate / changedate automatic from the Model base — declare nothing
BaseBone ("hidden") Hidden = Annotated[str, BoneType("hidden", replace=True)]
CaptchaBone / SpamBone request-time checks, nothing persisted — not applicable
RandomSliceBone query behaviour, not a field: def sqlFilter(self, stmt): return stmt.order_by(func.random())

Common bone parameters

Bone parameter Field equivalent
descr Field(descr=…) — defaults to the title-cased field name
required derived (non-optional without a default); override with required=
defaultValue default= / default_factory=
visible / readOnly visible=False / readonly=True (forces required: false)
params params={…}
languages Language[X] + languages=(…) or set_default_languages()
multiple constraints viur_relation_meta = {"rel": {"multiple": {"min": …, "max": …, "duplicates": …}}}
format format="$(dest.name)" — records and relations
refKeys viur_ref_keys = ("name",) on the target model (key/shortkey always included)
unique / indexed unique=True / index=… — enforced by SQL

Custom types and the pydantic ecosystem

# A bone type with no Python counterpart: one Annotated line
Slug = Annotated[str, BoneType("str.slug")]                # refines the str extras
Hidden = Annotated[str, BoneType("hidden", replace=True)]  # defines the bone alone

# Ecosystem types are registered — the validated type IS the declaration:
mail: EmailStr | None = Field(default=None)                # str.email
site: AnyUrl | None = Field(default=None, sa_type=String)  # uri
short: constr(max_length=12) | None = Field(default=None)  # str, maxlength 12

register_bone_type(MyType, BoneType("select.something"))   # MRO lookup, subclasses inherit

Also working natively: Strict*, PositiveInt/conint(gt=…) (exclusive bounds become exact min/max for ints), ByteSize, AwareDatetime/PastDate, extra-types Epoch.Integer and every str-subclass extra type (MacAddress, ISBN, TimeZoneName, ISO4217, …) as validated str bones. Do not use: SecretStr (masks dumps — take Password/Credential) and pydantic.Json (validates JSON strings — take viur.models.Json). Mind the normalizations: AnyUrl appends a slash, PhoneNumber stores RFC3966 (tel:+49…).