Skip to content
VibORM
Esc
navigateopen⌘Jpreview
On this page

JSON

JSON scalar type for structured data — untyped or typed with a StandardSchema validator

Basic Usage

import { s } from "viborm";

s.json(); // Required JSON (unknown type)
s.json().nullable(); // JSON | null

JSON scalars support .nullable(), .default(), .map(), and .schema() — but not .array(), .id(), or .unique(). See the support matrix and Native Types for column types (jsonb vs json on PostgreSQL, etc.).

Typed JSON

Provide a StandardSchema (Valibot, Zod, etc.) for type-safe JSON using the .schema() method:

import { object, picklist, boolean, string } from "valibot";

const settingsSchema = object({
  theme: picklist(["light", "dark"]),
  notifications: boolean(),
  language: string(),
});

s.json().schema(settingsSchema); // Typed as { theme: "light" | "dark"; ... }

Examples

Untyped JSON

const metadata = s.json().default({});
const tags = s.json().default([]);

Typed with Valibot

import {
  object,
  picklist,
  number,
  boolean,
  string,
  pipe,
  minValue,
  maxValue,
} from "valibot";

// User preferences
const preferencesSchema = object({
  theme: picklist(["light", "dark", "system"]),
  fontSize: pipe(number(), minValue(10), maxValue(24)),
  notifications: object({
    email: boolean(),
    push: boolean(),
  }),
});

const preferences = s
  .json()
  .schema(preferencesSchema)
  .default({
    theme: "system",
    fontSize: 14,
    notifications: { email: true, push: false },
  });

// Address structure
const addressSchema = object({
  street: string(),
  city: string(),
  country: string(),
  postalCode: string(),
});

const address = s.json().schema(addressSchema).nullable();

Complex Nested Types

import {
  object,
  string,
  number,
  array,
  optional,
  pipe,
  minValue,
  maxValue,
  integer,
  positive,
} from "valibot";

const orderItemSchema = object({
  productId: string(),
  quantity: pipe(integer(), positive()),
  price: pipe(number(), positive()),
  discount: optional(pipe(number(), minValue(0), maxValue(1))),
});

const orderSchema = object({
  items: array(orderItemSchema),
  shipping: addressSchema,
  notes: optional(string()),
});

const orderData = s.json().schema(orderSchema);

Querying JSON

JSON scalars support path-based filtering — see JSON Filters for the full operator list and per-database caveats. Note: JSON scalars do not support shorthand syntax. You must use explicit operations:

// Filter by nested value (must use { equals: value })
await client.user.findMany({
  where: {
    preferences: {
      path: ["theme"],
      equals: "dark",
    },
  },
});

// Updates must use { set: value }
await client.user.update({
  where: { id: "user_123" },
  data: {
    preferences: {
      set: { theme: "dark", fontSize: 16 },
    },
  },
});

Writing null

A nullable JSON column has two nulls: the database NULL (no document at all) and the JSON value null (a document that happens to be null). A bare null does not say which one you mean, so it is rejected in write position — at compile time and at runtime — and two exported sentinels name them instead:

import { DbNull, JsonNull } from "viborm";

// The column holds no document
await client.user.update({ where: { id }, data: { preferences: DbNull } });

// The column holds the JSON document `null`
await client.user.update({ where: { id }, data: { preferences: JsonNull } });

// Rejected: "null is ambiguous in JSON write data…"
await client.user.update({ where: { id }, data: { preferences: null } });

Only the top level is affected — { theme: null } is an ordinary document and stays legal. DbNull requires a .nullable() field; JsonNull works on any JSON field, since the JSON value null is a document like any other. AnyNull is filter-only: “either null” is a question, not a value.

The write type for a JSON field is exported as InputJsonValue (every JsonValue except a top-level null), which is useful when a fixture or helper needs to name it.

See JSON Filters for the matching filter side.

JSON vs Separate Scalars

Approach Pros Cons
JSON scalar Flexible, no migrations Harder to query, no constraints
Separate scalars Type-safe queries, indexes Schema changes need migrations
Related table Full SQL power More complex queries

Use JSON for user preferences, per-record metadata, third-party API responses, and audit logs. Use separate scalars for data you filter or sort by, data with strict validation, and data referenced in relations.

Was this page helpful?