# entity members

> The typed members an entity holds — text, numbers, dates, booleans, Guids, enums and references. A member's type spelling carries its nullability: a bare `string`, enum, `DateTime` or `Guid` is REQUIRED, a bare value type reads its zero, a reference is optional, and `?` makes any member optional. See [[types-optional-and-required]].

<!-- id: entity-properties · area: entity · stability: stable · html: https://osysharp.com/reference/entity/properties/ -->

## Summary        {#summary}
An entity's members are declared C#-style — `decimal Total;` — one per line, with optional attributes in front. Each
becomes a column. Whether a member is **required or optional is carried by its type spelling**, not a separate keyword:
a bare `string`, enum, `DateTime` or `Guid` is **required**, a bare value type (`int`, `bool`, `decimal`) reads its
zero, a **reference is optional**, and the `?` suffix makes any member optional. The full model — and *when* a required
member is checked — is [Optional and required members](https://osysharp.com/reference/types/optional-and-required/); the essentials are below.

## Signature      {#signature}
```osy syntax
entity <Name> {
  <attributes>? <Type> <MemberName>;
}
```

## Description    {#description}

### Which types may a member hold?   {#types}

**This table is the whole storable set** — [Every type, in one list](https://osysharp.com/reference/types/vocabulary/) is the same list with the non-storable types (the
collections, `Action`/`Func`, the component-parameter wrappers) beside it. If a type is not here and you did not
declare it yourself, an entity cannot hold it.

| Type | Holds | Notes |
|---|---|---|
| `string` | text | give it a `[MaxLength(n)]` — see below |
| `int` · `long` | whole numbers | `long` for ids and counters past 2³¹ — [long](https://osysharp.com/reference/types/long/) |
| `double` | measurements | a binary float — **never money** |
| `decimal` | money and exact quantities | **use this for money**, never a floating type — [decimal](https://osysharp.com/reference/types/decimal/) |
| `bool` | true / false | |
| `DateTime` | a point in time | stored UTC — [DateTime](https://osysharp.com/reference/types/datetime/) |
| `DateOnly` | a calendar date, no time | a SQL `date` — [DateOnly and TimeOnly](https://osysharp.com/reference/types/date-and-time/) |
| `TimeOnly` | a time of day, no date | a SQL `time` — [DateOnly and TimeOnly](https://osysharp.com/reference/types/date-and-time/) |
| `TimeSpan` | a duration | [TimeSpan (durations)](https://osysharp.com/reference/types/timespan/) |
| `Guid` | an identifier | |
| `Json` | an arbitrary JSON document | for genuinely open-shaped data — [Json](https://osysharp.com/reference/types/json/) |
| `Markdown` | a section-addressable markdown document | text plus a rendering contract — [Markdown](https://osysharp.com/reference/types/markdown/) |
| `RichText` | formatted prose | a rich-text document |
| `Vector` | an embedding, for similarity search | `[MaxLength(n)]` sets the dimensions — [[Searchable]](https://osysharp.com/reference/memory/searchable/) |
| `Zone` | an IANA time-zone token (`Europe/Stockholm`) | [Time zones — the Zone type and its operations](https://osysharp.com/reference/stdlib/zones/) |
| `Culture` | a BCP-47 culture token (`sv-SE`) | [Culture formatting — ToString(format, culture)](https://osysharp.com/reference/stdlib/culture-formatting/) |
| `byte[]` | binary | the one array form that is a scalar, not a collection |
| an `enum` you declared | a fixed set of values | see [enum](https://osysharp.com/reference/enum/declaration/) |
| another `entity` | a reference to one row | see [relations](https://osysharp.com/reference/entity/relations/) |
| `<Entity>[]` | the children pointing back at this row | see [relations](https://osysharp.com/reference/entity/relations/) |

```osy title="the scalar types" test app=entity-properties
entity Invoice {
  [MaxLength(40)] string Number;
  decimal Total;                 // money is ALWAYS decimal
  int LineCount;
  bool Paid;
  DateTime IssuedAt;
  Guid ExternalRef;
}
```

The date/time trio and `Json` are **ordinary columns like any other** — there is no conversion to write and no
`DateTime` to fall back on. Reach for `DateOnly` when the time of day is not a fact about the row (a birthday, an
invoice date, the day a slot is booked for), and `TimeOnly` when the date is not (opening hours, a daily cutoff):

```osy title="dates without times, times without dates, and open-shaped data" test app=entity-properties
entity Appointment {
  DateOnly Day;                  // a calendar date — no time of day to get wrong
  TimeOnly StartsAt;             // a time of day — no date attached
  TimeSpan Runs;                 // how long it lasts
  Json Extras;                   // genuinely open-shaped data, stored as a document
}

DateOnly WhenIsIt(Appointment a) { return a.Day; }   // and it reads back as itself
```

**A member may be named for its own type**, exactly as `Color Color;` is in C# — and it is the natural spelling for
the commonest fields (`Room Room`, `Status Status`, `Type Type`). No prefix, no suffix, no second word:

```osy title="a member named for its own type — C#'s `Color Color`" test app=entity-properties
enum Room { Kitchen, Bathroom, Bedroom }

entity Kiln {
  [Required, MaxLength(80)] string Name;
  [Required] Room Room;          // the member and its type share a name — legal, and the name to use
}

Room WhereIsIt(Kiln p) { return p.Room; }   // and it reads back with no ceremony
```

### Is this member required, or may it be null?   {#optional}
Whether a member is required is decided by **how you spell its type** — there is no separate keyword. What a *bare*
(non-`?`) member means depends on whether its type has a natural zero (full treatment: [Optional and required members](https://osysharp.com/reference/types/optional-and-required/)):

- A **bare `string`, enum, `DateTime`/`DateOnly`/`TimeOnly`, `Guid` or `Json`** — a type with **no honest zero** — is
  **required**: the platform invents no value for it, so you must supply one. (An enum is *not* silently defaulted to
  its first member — reordering the members would change the stored default — so a bare enum is required too.)
- A **bare value-type scalar** — `int`, `long`, `bool`, `decimal`, `TimeSpan` — reads its **zero** (`0`, `false`, `0m`)
  when unset, exactly as a C# field does. It is never null; declare it `int?` for a real "unset". (Because it is never
  null, comparing one to `null` — `priority == null` — is a compile error that points you at the `?` form.)
- A **bare entity reference is optional** (reads back `null`) — the everyday shape is *create the row, then pick the
  related record* — and you write `[Required]` to demand one.
- The **`?` suffix** makes any member optional; it reads back `null` when nobody set it.

**When is a required member checked?** An entity is a **draft until commit**, so `new Ticket {}` compiles — you seed an
empty draft, bind each field to a form input, and the required members are validated **at commit**, naming any that are
still unset. (A `class` has no commit step, so its required members are checked at `new` instead — see
[class properties](https://osysharp.com/reference/class/properties/).) This is what makes the everyday create-form work.

```osy title="required by spelling; optional with ?" test app=entity-properties
entity Contact {
  string Name;              // REQUIRED — a bare string has no honest zero (checked at commit)
  string? Phone;            // optional — a contact without a phone is fine; reads back null
  DateTime? LastSpokeAt;    // `?` is not a string thing: ANY type takes it, value types included
  int? Doorstep;            // `int?` is genuinely absent, which is what `0` could never say
}

string PhoneOrDash(Contact c) {
  return c.Phone ?? "—";    // null-coalescing on the optional field, exactly as in C#
}
```

⚑ **EVERY C# NULLABLE TYPE IS SUPPORTED.** `?` is not a string affordance — it is the C# rule, and it holds for
value types and reference types alike: `DateTime?`, `int?`, `decimal?`, `bool?`, `Guid?`, `TimeOnly?`, an enum,
your own `class`. There is no list to check against, which is why this page states the rule rather than
enumerating one.

⚠ **It is written down because the shape is usually only ever SHOWN on a string**, and a reader who has seen
`string?` and nothing else has to guess. The guess costs a field: measured 2026-09-01, a model reasoning aloud —
*"`DateTime?` — is nullable DateTime supported? The docs mention `string?` for nullable"* — and it dropped the
field rather than find out.

A bare **reference** is the exception — optional by default, so write `[Required]` to demand one:
`[Required] Customer Reporter;`. See [relations](https://osysharp.com/reference/entity/relations/) and [Optional and required members](https://osysharp.com/reference/types/optional-and-required/).

### How do I give a member a default value?   {#defaults}
A member may declare a default, which applies when the row is created without one — `= true`, `= 0m`, or an enum
member. It is the honest way to say *"this is what a new one looks like"*, instead of remembering to set it at every
creation site:

```osy title="what a new row looks like" test app=entity-properties
enum AccountStatus { Active, Suspended, Closed }

entity Account {
  [Required, MaxLength(200)] string Name;
  bool IsActive = true;                          // a new account is active
  AccountStatus Status = AccountStatus.Active;   // …and its status says so
  decimal Balance = 0m;
  [Required] string ApiKey = Security.RandomId(32);   // a fresh unguessable key per account
  DateTime CreatedFor = DateTime.UtcNow.AddDays(30);     // …and a computed date, evaluated at creation
}

void Open(string name) {
  var a = new Account { Name = name };
  // IsActive is true, Status is Active, Balance is 0 — none of them written here;
  // ApiKey is a fresh 32-char id and CreatedFor is 30 days out — each EVALUATED for this new row
}
```

A default is not limited to a constant. It can be **any expression** — a call like `Security.RandomId(32)`, a
computed value like `DateTime.UtcNow.AddDays(30)`, arithmetic — and it is **evaluated afresh for each row at creation**,
exactly like a C# field initializer. So two accounts opened in the same breath get two *different* `ApiKey`s; the
expression runs per row, not once. (A constant/enum default behaves the same as always.)

Reach for a default whenever "unset" and "the normal value" are the same thing. It removes a whole class of bug:
the creation site somebody added last week that forgot to set `IsActive`, and the row that has been invisible ever
since.

### Text needs a length   {#maxlength}
`string` with no `[MaxLength]` is unbounded. That is fine for a body of prose and wrong for a code, a name or a
status — give those a length, and the database enforces it:

```osy title="bounded and unbounded text" test app=entity-properties
entity Article {
  [MaxLength(200)] string Title;   // bounded — a title has a sane maximum
  string Body;                     // unbounded — prose
}
```

### Money is `decimal`   {#money}
There is no `float` or `double` member type, and that is on purpose: binary floating point cannot represent `0.10`,
so totals drift by fractions of a cent and eventually a customer notices. `decimal` is exact.

### How do I index a member, or rename its column?   {#storage-attributes}
A handful of attributes shape how a member is *stored* rather than what it may hold. Most rows never need them:

- **`[Index]`** asks the database to index the member, so queries that filter or sort by it stay fast as the table
  grows. Put it on the members you actually query by.
- **`[ExternalName]`** overrides the database column name when it must differ from the member — `[ExternalName("col")]`
  maps onto a pre-existing or externally-owned schema.
- **`[Virtual]`** marks a member **in-memory only**: it is never persisted, for a value you compute and carry but do
  not store.
- **`[Id]`** marks a member as the entity's primary key. Every entity already has an automatic `Id`, so you only reach
  for this to supply your own key instead of the default.
- **`[DynamicType]`** lets a member's type be resolved at runtime rather than fixed at declaration — an advanced escape
  hatch for genuinely polymorphic storage.
- **`[Mentions]`** declares the entities a rich-text member may `@`-mention, so an editor can offer and resolve them.

```osy title="indexing and an external column name" test app=entity-properties
entity LegacyCustomer {
  [Required, Index] string Status;              // queried often → indexed
  [ExternalName("cust_ref")] string Reference;  // the column is named cust_ref in the database
  [Virtual] int ScoreThisSession;               // computed and carried, never stored
}
```

## See also       {#see-also}
- [entity](https://osysharp.com/reference/entity/declaration/) — the type these members live in
- [constraints](https://osysharp.com/reference/entity/constraints/) — `[Required]`, `[Unique]`, `[MaxLength]`, `[Min]`/`[Max]`, `[Pattern]`
- [relations](https://osysharp.com/reference/entity/relations/) — members that point at another entity
- [enum](https://osysharp.com/reference/enum/declaration/) — a member with a fixed set of values
