Skip to main content

Fields

Fields are how a node stores data. Because Stof is a JSON superset, plain JSON key-value pairs are already valid fields — everything below is what you can layer on top of that when you want more control.

Declaration Styles

JSON-style, shorthand, and explicitly typed declarations are all the same thing under the hood:

declarations.stof
"json_style": "still valid"
shorthand: "no quotes needed"
str typed: "explicit type"

#[main]
fn main() {
  pln(self.json_style, self.shorthand, self.typed);
}
Output

Types & Casting

A typed field keeps its type — assigning a new value casts it, rather than replacing the type:

casting.stof
str field: "temp"

#[main]
fn main() {
  self.field = 42;
  pln(self.field, typeof self.field);
}
Output

42 becomes the string "42" on assignment, because field was declared str.

Const Fields

const throws on write instead of silently accepting it:

const.stof
const str locked: "permanent"

#[main]
fn main() {
  try { self.locked = "changed"; }
  catch { pln('blocked'); }

  pln(self.locked);
}
Output

Access Modifiers

#[readonly] allows reads from anywhere but throws on write. #[private] restricts both — visible only to the object that defines it, not children, not parents:

visibility.stof
Inner: {
  #[private]
  secret: 'hidden'

  fn revealed() -> str {
      self.secret
  }
}

#[main]
fn main() {
  pln(self.Inner.revealed());
  pln(self.Inner.secret);
}
Output

revealed() runs on Inner itself, so it can see secret. main() is calling in from outside Inner, so the second pln prints null.

Non-Null Fields

A ! suffix on a type makes a field reject null; without one, any typed field can still hold null:

nonnull.stof
list! required: [1, 2, 3]
obj optional: null

#[main]
fn main() {
  pln(self.required, self.optional);
}
Output

Assigning null to required would throw. Null & Initialization covers the rest of how nullability works, including ?? and null-safe access.

Union & Tuple Fields

A field can accept more than one type. Assigning a value outside the union casts it to the first matching type:

union.stof
bool | int flexible: false
(str, ver) release: ('beta', 0.5.24)

#[main]
fn main() {
  self.flexible = 'not in union';
  pln(self.flexible, self.release);
}
Output

'not in union' isn't a bool or an int, so it gets cast to the first matching type — bool — rather than rejected.

Creating Fields Dynamically

Assigning to a path creates whatever doesn't exist yet along the way — fields, nodes/objects, even new roots:

dynamic.stof
#[main]
fn main() {
  self.created = true;
  pln(self.created);

  root.NewSpace.nested = 'auto-created';
  pln(root.NewSpace.nested);
}
Output

NewSpace didn't exist before this ran — assigning root.NewSpace.nested created both the node and the field in one step.