dataType (asset type)
A dataType is a user-authored typed-data contract: it declares a set of fields — names, types, constraints, defaults — that any bound .data instance must satisfy. A contract can carry an optional…
When to use one
- You want a typed, validated configuration shape (a weapon's damage /
fire mode / recoil profile, a quest's objectives, a loot table's
entries) that many
.datainstances can be authored against. - You want field-level validation (required, numeric ranges, enums, references to other assets or data instances) enforced the same way everywhere the contract is used.
- You want shared structure across related contracts without
duplicating field declarations —
extendsfactors common fields into a parent contract.
If you only need a snapshot of one component's properties, use a
.preset instead. If you need executable shared logic rather than
typed configuration, use a .module.
Where it lives
- Source:
/zero/source/.../<Name>.dataType/ - Identity:
<Name>(the.dataTypesuffix strips). - Folder shape:
schema.yaml— the field declaration (fields:, optionalextends:). Required.README.md— instance documentation: what the contract models, what each field means. Required..metadata— description + tags for search. Required.behavior.luau— optional ref methods this contract's.datainstances expose beyond the built-in:contract/:satisfies/:validatesurface.
How to create one
asset.create("dataType", "<Name>")
-- Creates: /zero/source/<Name>.dataType/
-- schema.yaml (empty `fields:` scaffold)
-- README.md (instance README template)
-- .metadata (empty description/tags — fill both to publish)
Then edit schema.yaml to declare the contract's fields.
schema.yaml field-spec vocabulary
extends: "item" # optional: parent contract identity
fields:
damage:
type: number
required: true
min: 0
max: 100
fireMode:
type: string
enum: ["semi", "burst", "auto"]
default: "semi"
muzzle:
type: vec3
default: [0, 0, 0]
tint:
type: color # "#rrggbb"/"#rrggbbaa" or {r,g,b[,a]}
fireSound:
type: assetRef
assetType: soundClip # required for assetRef fields
recoil:
type: dataRef
contract: recoilProfile # required for dataRef fields
required: true
falloff:
type: array
items:
type: struct
fields:
distance: { type: number }
multiplier: { type: number }
Field types: number, string, boolean, vec3, color, assetRef,
dataRef, array, struct.
required— a boolean; a field with adefaultis never "missing" even whenrequiredis set.default— must itself satisfy the field's own constraints (checked at declaration time).min/max— apply only tonumberfields.enum— apply only to scalar fields (number/string/boolean); the value must equal one of the listed entries.assetRef— needs anassetType; validated againstasset.resolve(identity, assetType).dataRef— needs acontract; validated against whether the referenced.datainstance's contract chain includes it (:satisfies).array— needs anitemsfield spec every element must satisfy.struct— needs afieldsmap of nested field specs; struct values reject unknown keys the same way top-level values do.
extends
A contract may declare extends: "<parentContract>" to inherit the
parent's fields. The child adds new fields — it never redeclares a
field the parent (or any ancestor) already declared; doing so is a
schema error. Behavior is optional at every level: a contract may
carry behavior.luau or not regardless of whether its parent does.
Chains are single-inheritance and acyclic — a cycle raises loudly the
moment the chain is resolved (:schema, :validate, :defaults,
:chain all resolve the chain). Editing a parent's schema does not
re-warn its child contracts at edit time; validation stays loud where
the chain is used — :schema / :validate calls and the publish gate.
Ref surface (AssetRef<dataType>)
ref:schema()— the merged field map (own fields + every field inherited throughextends).ref:validate(values)—(ok, violations)against the merged schema.violationsis an array of{ path, message }.ref:defaults()— the default value table the merged schema declares (a fresh, mutable table).ref:parent()— the parent contract ref (extends), or nil for a root contract.ref:chain()— the full extends chain as refs, root parent first, this contract last.ref:instances()— every.datainstance bound to this contract or to a contract that extends it, asAssetRefs. A full scan over the world's data assets — a discovery surface for authoring and tooling, not a per-frame call.
Discovery
asset.list("dataType")— every registered contract, asAssetRefs.asset.inspect("<name>")— the contract's files, declared schema body, instance README, and this type README.cat /zero/source/<Name>.dataType— same summary.
Authoring conventions
- Name contracts for the concept they type (
weapon,questObjective,lootTable), not for a specific instance. - Factor genuinely shared fields into a parent contract via
extends; don't duplicate a field declaration across sibling contracts. - Document every field's meaning and units in
README.md— the schema only carries types and constraints, not intent.
Common pitfalls
- Redeclaring an inherited field.
extendsmerges parent fields in; a child that re-declares a field name the parent already declared is a merge error, not a silent override. - Cyclic
extends. Contract A extending B extending A raises loudly the first time the chain is resolved — fix the chain, there is no partial result to fall back to. - assetRef/dataRef without existence checks.
assetTypeandcontracton ref fields are declaration-time requirements; the referenced asset or data instance is checked at validation time, not declaration time.
Related types
.data— a configured value instance bound to a dataType contract..preset— a snapshot of one component's properties, not a typed multi-field contract..component— executable entity behavior, versus this type's typed configuration data.