dataType (asset type)
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 named set of values for settings something already
declares, 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 (`fields: {}`, a valid contract with no fields yet)
-- 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.ls /zero/source/<Name>.dataTypelists the files the folder holds.
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 named set of values for one declarer's settings schema (an assetType, an importer, a component), not a contract of its own..component— executable entity behavior, versus this type's typed configuration data.
In the Inspector
Contract: the contract it extends (a link), every field across the extends chain with its type, whether it is required, its default and its bounds or allowed values, and Count instances, which scans every data asset for the ones that satisfy it (a count taken before a data asset was made or removed says so).