Cerial
Decorators

@flexible

Allow object-type fields to store arbitrary extra properties alongside defined typed fields.

Marks an object-type field as flexible, allowing it to store arbitrary extra fields alongside the defined typed fields. Known fields retain their type safety, while unknown fields are accepted as any.

Syntax

model User {
  id Record @id
  address Address @flexible
}

@flexible takes no arguments and can only be applied to fields whose type is an object (not scalars, arrays of scalars, or relations).

When to Use

Use @flexible when a field needs a schema-defined core structure but must also accept dynamic, user-defined properties:

  • User preferences with a known base shape but extensible settings
  • Metadata fields where known keys are typed but additional keys vary per record
  • Integration payloads with a stable core and provider-specific extras
  • Feature flags with known defaults and dynamic overrides

Basic Usage

object Address {
  street String
  city String
  zip String?
}

model Customer {
  id Record @id
  name String
  billing Address @flexible
  shipping Address
}

billing is flexible — it accepts street, city, zip plus any extra keys. shipping uses the same Address object but is strict — only street, city, and zip are allowed.

// Create with extra fields on the flexible field
const customer = await client.db.Customer.create({
  data: {
    name: 'Alice',
    billing: { street: '123 Main', city: 'NYC', floor: 5, buzzer: 'A3' },
    shipping: { street: '456 Oak', city: 'LA' },
  },
});

customer.billing.street; // string — typed
customer.billing.floor;  // any — extra field, accessible
customer.billing.buzzer; // any — extra field, accessible

Field-Level, Not Object-Level

@flexible is applied per field, not on the object definition itself. The same object type can be flexible on one field and strict on another:

object Metadata {
  version Int
}

model Document {
  id Record @id
  config Metadata @flexible   // accepts extra keys
  audit Metadata              // strict — version only
}

This means you can use a single object definition with different flexibility levels across your models.

Type Behavior

The @flexible decorator affects generated TypeScript types by intersecting the base object interface with Record<string, any>:

AspectStrict fieldFlexible field
InterfaceAddressAddress & Record<string, any>
InputAddressInputAddressInput & Record<string, any>
CreateKnown fields onlyKnown fields + arbitrary extras
Update (merge)Partial<AddressInput>Partial<AddressInput & Record<string, any>>
Update (set){ set: AddressInput }{ set: AddressInput & Record<string, any> }
WhereAddressWhereAddressWhere & { [key: string]: any }
Return typeAddressAddress & Record<string, any>
Select / OrderByKnown fields onlyKnown fields only

Known fields retain full type safety. Extra fields are typed as any.

Nested Flexible

@flexible can be applied to object-typed fields within object definitions:

object Tags {
  label String
}

object Profile {
  bio String
  tags Tags @flexible
}

model User {
  id Record @id
  profile Profile
}

Here profile itself is strict (only bio and tags), but profile.tags is flexible — it accepts label plus extra keys.

Optional and Array Fields

@flexible works with optional fields and array fields:

model User {
  id Record @id
  address Address @flexible          // required flexible
  shipping Address? @flexible        // optional flexible
  locations Address[] @flexible      // array of flexible objects
}
// Optional flexible — can be omitted
await client.db.User.create({
  data: {
    address: { street: 'Main', city: 'NYC', custom: true },
  },
});

// Array of flexible objects — each element accepts extras
await client.db.User.create({
  data: {
    address: { street: 'Main', city: 'NYC' },
    locations: [
      { street: 'Home', city: 'NYC', floor: 3 },
      { street: 'Work', city: 'SF', desk: 'A5' },
    ],
  },
});

Querying Flexible Fields

Filtering Known Fields

Known fields support all standard operators:

const results = await client.db.Customer.findMany({
  where: { billing: { city: { contains: 'NY' } } },
});

Filtering Extra Fields

Extra fields can be filtered using direct equality or operator objects. Since the Where type includes & { [key: string]: any }, you can pass any additional keys:

// Equality on extra field
const results = await client.db.Customer.findMany({
  where: { billing: { floor: 5 } },
});

// Operator on extra field
const results2 = await client.db.Customer.findMany({
  where: { billing: { floor: { gt: 3 } } },
});

Since extra field types are any, the runtime passes values through to SurrealDB without type validation. SurrealDB handles the comparison at the database level.

Updates

Merge Update

Partial updates merge with existing data. Extra fields are preserved:

await client.db.Customer.updateUnique({
  where: { id: customer.id },
  data: { billing: { city: 'SF' } },
});
// billing.street preserved, billing.floor preserved, city changed to SF

Set (Full Replace)

Replaces the entire object. Old extra fields are removed:

await client.db.Customer.updateUnique({
  where: { id: customer.id },
  data: { billing: { set: { street: 'New St', city: 'LA', newProp: true } } },
});
// old floor/buzzer gone, newProp added

Allowed On

ConstructAllowed
Model fields (object type)
Object fields (nested object type)
Non-object fields
Tuple elements
Literal fields

Restrictions

  • @flexible can only be applied to fields with an object type (not scalars, relations, or arrays of scalars)
  • Cannot be applied on the object definition itself — it is always per-field
  • Select and OrderBy only support known fields (extra fields are not included)

Mutual Exclusions

@flexible has no conflicts with other decorators. Its only constraint is that the field must be an object type.

On this page