JSON Columns in Sequelize: DataTypes.JSON, Queries, and TypeScript

Last updated:

Sequelize maps JSON database columns to JavaScript objects automatically using DataTypes.JSON (MySQL, MariaDB, SQLite, PostgreSQL) or DataTypes.JSONB (PostgreSQL only). Sequelize handles serialization on write and parsing on read — no manual JSON.stringify or JSON.parse needed. Define a model attribute with type: DataTypes.JSONB in the model definition, and Sequelize serializes JS objects to JSON strings on insert and parses them back on select. DataTypes.JSONB maps to PostgreSQL's binary JSON format, which supports GIN indexing and path operator queries 10–30% faster than text json. For querying nested JSON values, Sequelize provides the where() function with Sequelize.literal() for raw SQL path expressions, since most JSON path operators aren't abstracted in Sequelize's Op object. This guide covers model definitions, DataTypes.JSONB vs DataTypes.JSON, querying with literal() and Op.contains, TypeScript interface mapping, GIN index migrations, and nested JSON update patterns.

Defining a JSON Column with DataTypes.JSONB

Bottom line: use DataTypes.JSONB for PostgreSQL and DataTypes.JSON for MySQL, MariaDB, and SQLite. Sequelize calls JSON.stringify on insert and JSON.parse on select — you work with plain JavaScript objects at all times.

Both DataTypes.JSON and DataTypes.JSONB are imported from "sequelize". In a sequelize.define() call, set type: DataTypes.JSONB for the column attribute. In class-based model syntax (used with TypeScript), declare the field and add it to the class's init() call. The allowNull: false flag adds a NOT NULL constraint. defaultValue: {} sets the column default at the database level. DataTypes.JSONB maps to PostgreSQL's binary jsonb type, which stores a pre-parsed binary representation. On a 1M row table, a jsonb column query with a GIN index runs in ~4ms; without the index, the same query takes ~800ms. DataTypes.JSON stores the text verbatim and re-parses on every read — useful when exact byte fidelity (key order, whitespace) is required, but slower for queries.

import { Sequelize, DataTypes, Model, InferAttributes, InferCreationAttributes, CreationOptional } from "sequelize"

const sequelize = new Sequelize("postgres://user:pass@localhost:5432/mydb")

// --- TypeScript interfaces for the JSON columns ---
interface UserMetadata {
  plan: "free" | "pro" | "enterprise"
  features: string[]
  createdAt: string
}

interface OrderLineItem {
  productId: number
  quantity: number
  price: number
}

// --- Class-based model with DataTypes.JSONB (PostgreSQL) ---
class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
  declare id: CreationOptional<number>
  declare email: string
  declare metadata: UserMetadata | null
}

User.init(
  {
    id:    { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
    email: { type: DataTypes.STRING, allowNull: false, unique: true },
    // DataTypes.JSONB: binary storage, GIN-indexable, PostgreSQL only
    metadata: {
      type:         DataTypes.JSONB,
      allowNull:    true,
      defaultValue: null,
    },
  },
  { sequelize, tableName: "users" }
)

// --- Order model: jsonb array-of-objects column ---
class Order extends Model<InferAttributes<Order>, InferCreationAttributes<Order>> {
  declare id: CreationOptional<number>
  declare userId: number
  declare lineItems: OrderLineItem[]
}

Order.init(
  {
    id:        { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
    userId:    { type: DataTypes.INTEGER, allowNull: false },
    lineItems: { type: DataTypes.JSONB,   allowNull: false, defaultValue: [] },
  },
  { sequelize, tableName: "orders" }
)

// --- sequelize.define() style (MySQL uses DataTypes.JSON) ---
const Product = sequelize.define("Product", {
  name:       { type: DataTypes.STRING,  allowNull: false },
  // DataTypes.JSON: works in MySQL, MariaDB, SQLite, and PostgreSQL
  attributes: { type: DataTypes.JSON,   allowNull: true },
})

// --- Insert: pass a plain JS object — Sequelize serializes it ---
await User.create({
  email:    "alice@example.com",
  metadata: { plan: "pro", features: ["analytics", "api-access"], createdAt: new Date().toISOString() },
})

// --- Select: metadata is deserialized back to UserMetadata | null ---
const users = await User.findAll()
console.log(users[0].metadata?.plan)  // "pro"

TypeScript Type Safety with Sequelize Models

Bottom line: declare JSON columns with a specific interface type in your model class. With plain Sequelize, use the InferAttributes generic pattern. With sequelize-typescript, use the @Column decorator. TypeScript checks are compile-time only — no runtime enforcement from the ORM.

Without a typed declaration, Sequelize infers JSON columns as object, which loses shape information. The recommended approach with Sequelize 6+ and TypeScript is to use InferAttributes<Model> and InferCreationAttributes<Model> generics with declare field declarations. These declarations carry your specific TypeScript interface and are erased at runtime — they produce 0 bytes of JavaScript output. With sequelize-typescript (a decorator-based wrapper), the @Column({ type: DataTypes.JSONB }) decorator sets the Sequelize column type while the TypeScript field declaration sets the compile-time type. The @IsObject() and @ValidateNested() decorators from class-validator add runtime validation before each save, but require explicit await instance.validate() or validate: true in the save options.

// --- Plain Sequelize 6 + TypeScript (recommended) ---
import {
  Model, DataTypes, Sequelize,
  InferAttributes, InferCreationAttributes, CreationOptional,
} from "sequelize"

interface ProductMetadata {
  sku: string
  weight: number          // grams
  tags: string[]
  discontinued: boolean
}

class Product extends Model<InferAttributes<Product>, InferCreationAttributes<Product>> {
  declare id:       CreationOptional<number>
  declare name:     string
  // Specific interface: compile-time type is ProductMetadata, not just object
  declare metadata: ProductMetadata
}

Product.init(
  {
    id:       { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
    name:     { type: DataTypes.STRING,  allowNull: false },
    metadata: { type: DataTypes.JSONB,   allowNull: false },
  },
  { sequelize, tableName: "products" }
)

// TypeScript now enforces the shape on insert:
await Product.create({
  name: "Widget",
  metadata: {
    sku:          "W-001",
    weight:       250,
    tags:         ["hardware", "tool"],
    discontinued: false,
  },
})

// And on select: product.metadata is typed as ProductMetadata
const product = await Product.findByPk(1)
console.log(product?.metadata.sku)   // "W-001"

// --- sequelize-typescript decorator syntax ---
// npm install sequelize-typescript reflect-metadata
import { Table, Column, Model as STModel, DataType } from "sequelize-typescript"

interface Address {
  street: string
  city:   string
  zip:    string
}

@Table({ tableName: "customers" })
class Customer extends STModel {
  @Column(DataType.STRING)
  declare email: string

  // @Column with DataType.JSONB — TypeScript type set by the declare field
  @Column({ type: DataType.JSONB, allowNull: true })
  declare address: Address | null
}

// TypeScript: customer.address is Address | null
const customer = await Customer.findByPk(1)
const city = customer?.address?.city  // typed as string | undefined

Querying JSON Fields with Operators and literal()

Bottom line: use Op.contains for PostgreSQL JSONB containment (@>) and Sequelize.literal() for all other JSON path operators — Sequelize doesn't abstract ->, ->>, or JSON_EXTRACT in the Op set.

The Op.contains operator is the only built-in Sequelize helper that maps to a JSON operator — it generates @> for JSONB columns. For all other path expressions, wrap raw SQL in Sequelize.literal() and pass it to the where option. PostgreSQL's ->>'key' extracts a JSON field as text, suitable for equality comparisons. MySQL's JSON_EXTRACT(col, '$.key') does the same. For ORDER BY a JSON field, pass aliteral() in the order array. The table below summarizes 5 common query patterns across PostgreSQL and MySQL:

GoalPostgreSQL (JSONB)MySQL (JSON)
Extract field as textliteral("metadata->>'plan'")literal("JSON_EXTRACT(metadata, '$.plan')")
Containment checkwhere(col('metadata'), Op.contains, { plan: 'pro' })Not supported (no @>)
Nested pathliteral("metadata->'address'->>'city'")literal("JSON_EXTRACT(metadata, '$.address.city')")
Order by JSON fieldorder: [[literal("metadata->>'score'"), 'DESC']]order: [[literal("JSON_EXTRACT(metadata, '$.score')"), 'DESC']]
Key existenceliteral("metadata ? 'tags'")literal("JSON_CONTAINS_PATH(metadata, 'one', '$.tags')")
import { Op, where, col, literal, fn } from "sequelize"
import { User } from "./models"

// --- Op.contains: JSONB @> containment (PostgreSQL only) ---
// Finds users where metadata contains { plan: "pro" }
const proUsers = await User.findAll({
  where: {
    metadata: { [Op.contains]: { plan: "pro" } },
  },
})

// --- literal() with ->>: text field extraction (PostgreSQL) ---
const proUsers2 = await User.findAll({
  where: where(literal("metadata->>'plan'"), "pro"),
})

// --- MySQL: JSON_EXTRACT ---
// const mysqlUsers = await User.findAll({
//   where: where(literal("JSON_EXTRACT(metadata, '$.plan')"), "pro"),
// })

// --- Nested path: metadata->'address'->>'city' ---
const berlinUsers = await User.findAll({
  where: where(literal("metadata->'address'->>'city'"), "Berlin"),
})

// --- Combine with Sequelize Op operators ---
const activeProUsers = await User.findAll({
  where: {
    [Op.and]: [
      where(literal("metadata->>'plan'"), { [Op.ne]: "free" }),
      where(literal("metadata->>'active'"), "true"),
    ],
  },
})

// --- Order by JSON field ---
const topUsers = await User.findAll({
  order: [[literal("metadata->>'score'"), "DESC"]],
  limit: 10,
})

// --- Select only a JSON subfield ---
const plans = await User.findAll({
  attributes: [
    "email",
    [literal("metadata->>'plan'"), "plan"],
  ],
})

GIN Indexes for jsonb Performance

Bottom line: Sequelize has no dedicated JSONB index decorator — add GIN indexes in raw migrations using queryInterface.addIndex() with using: 'gin' or a raw SQL query for jsonb_path_ops. A GIN index reduces containment query time from ~800ms to ~4ms on a 1M row table.

GIN (Generalized Inverted Index) decomposes each JSONB value into its key-value pairs and builds an inverted index mapping each element to the rows containing it. This makes @> containment and ? existence queries O(log n) instead of O(n). There are 2 GIN operator classes to choose from: the default GIN supports all operators (@>, <@, ?, ?|, ?&). jsonb_path_ops supports only @> but produces a smaller, faster index — typically 20–40% smaller than the default GIN. Use jsonb_path_ops when your queries exclusively use containment. GIN indexes have higher write overhead than B-tree indexes: inserts and updates are 2–3× slower on columns with GIN indexes. They are best for JSONB columns that are queried frequently but updated less often.

// --- Migration: add a GIN index on the metadata JSONB column ---
// File: migrations/20260527-add-gin-index-users-metadata.js

"use strict"

module.exports = {
  async up(queryInterface, Sequelize) {
    // Default GIN: supports @>, <@, ?, ?|, ?&
    await queryInterface.addIndex("users", ["metadata"], {
      name:  "idx_users_metadata_gin",
      using: "gin",
    })
    // Equivalent SQL:
    // CREATE INDEX idx_users_metadata_gin ON users USING gin(metadata);
  },

  async down(queryInterface, Sequelize) {
    await queryInterface.removeIndex("users", "idx_users_metadata_gin")
  },
}

// --- jsonb_path_ops GIN: containment only, 20–40% smaller ---
// queryInterface.addIndex doesn't support operator classes directly.
// Use a raw query instead:
module.exports = {
  async up(queryInterface, Sequelize) {
    await queryInterface.sequelize.query(
      "CREATE INDEX idx_users_metadata_gin_po ON users USING gin(metadata jsonb_path_ops)"
    )
  },
  async down(queryInterface, Sequelize) {
    await queryInterface.sequelize.query(
      "DROP INDEX IF EXISTS idx_users_metadata_gin_po"
    )
  },
}

// --- B-tree expression index on a specific JSON path ---
// Faster than GIN for single-field equality queries (no GIN overhead)
module.exports = {
  async up(queryInterface, Sequelize) {
    await queryInterface.sequelize.query(
      "CREATE INDEX idx_users_plan ON users ((metadata->>'plan'))"
    )
  },
  async down(queryInterface, Sequelize) {
    await queryInterface.sequelize.query(
      "DROP INDEX IF EXISTS idx_users_plan"
    )
  },
}

// --- Verify the index is used: EXPLAIN ANALYZE ---
// EXPLAIN ANALYZE SELECT * FROM users WHERE metadata @> '{"plan":"pro"}';
// With GIN:    Bitmap Index Scan on idx_users_metadata_gin  (cost=0.00..4.01 rows=1)
// Without GIN: Seq Scan on users  (cost=0.00..2893.00 rows=500000)

Partial JSON Updates

Bottom line: Sequelize has no atomic in-place JSON update API. Use fetch-mutate-save for simplicity, add optimistic locking with a version column for concurrency safety, or use Model.update() with sequelize.literal("jsonb_set(...)") for atomic server-side updates without a round-trip.

The fetch-mutate-save pattern reads the row, spreads the existing JSON object with the new value in JavaScript, and writes it back. This requires 2 database round-trips and risks a race condition if 2 processes update the same row concurrently. Sequelize's built-in optimistic locking adds a version integer column — set version: true in the model options, and Sequelize automatically adds WHERE version = N to every UPDATE and increments the version. If another process saves first, Sequelize throws an OptimisticLockError (retry or surface to the user). For high-throughput scenarios where you need a single round-trip, use PostgreSQL's jsonb_set() via sequelize.literal() in a Model.update() call — this is an atomic UPDATE that never reads the row into application memory. jsonb_set(target, path, value, create_if_missing) takes 4 arguments: the column, a path array literal, the new JSON value, and whether to create the path if missing.

import { Op } from "sequelize"
import { User } from "./models"
import { sequelize } from "./db"

// --- Pattern 1: fetch-mutate-save (simple, race condition risk) ---
async function upgradePlan(userId: number, newPlan: "pro" | "enterprise") {
  const user = await User.findByPk(userId)
  if (!user || !user.metadata) throw new Error("User not found")

  user.metadata = { ...user.metadata, plan: newPlan }
  await user.save()
}

// --- Pattern 2: Optimistic locking with version column ---
// Add to model init: { version: true } in model options
// Sequelize adds: version: { type: DataTypes.INTEGER, defaultValue: 0 }

User.init(
  {
    id:       { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
    email:    { type: DataTypes.STRING,  allowNull: false },
    metadata: { type: DataTypes.JSONB,   allowNull: true },
    // version column managed automatically by Sequelize
  },
  { sequelize, tableName: "users", version: true }
)

async function upgradePlanSafe(userId: number, newPlan: "pro" | "enterprise") {
  try {
    const user = await User.findByPk(userId)
    if (!user || !user.metadata) throw new Error("User not found")

    user.metadata = { ...user.metadata, plan: newPlan }
    await user.save()   // throws OptimisticLockError if another process saved first
  } catch (err: any) {
    if (err.name === "SequelizeOptimisticLockError") {
      // retry logic here — re-read and try again
      console.warn("Concurrent update detected, retrying...")
    }
    throw err
  }
}

// --- Pattern 3: Atomic jsonb_set() — no round-trip (safest for high concurrency) ---
// jsonb_set(target, path_array, new_value, create_if_missing)
await User.update(
  {
    metadata: sequelize.literal(
      // Sets metadata.plan = "pro" atomically at the database level
      "jsonb_set(metadata, '{plan}', '"pro"'::jsonb, true)"
    ) as any,
  },
  { where: { id: 1 } }
)

// --- Nested path: update metadata.address.city ---
await User.update(
  {
    metadata: sequelize.literal(
      "jsonb_set(metadata, '{address,city}', '"Berlin"'::jsonb, true)"
    ) as any,
  },
  { where: { id: 1 } }
)

// --- Append to a JSONB array field ---
await User.update(
  {
    metadata: sequelize.literal(
      "jsonb_set(metadata, '{features}', (metadata->'features') || '["sso"]'::jsonb)"
    ) as any,
  },
  { where: { id: 1 } }
)

Migrations and Schema Changes for JSON Columns

Bottom line: use queryInterface.addColumn() with DataTypes.JSONB to add a JSON column in a migration. JSON columns enforce no schema at the database level — all shape validation happens in the application layer. Use queryInterface.bulkUpdate() to backfill existing rows when adding a new JSON column derived from other columns.

Adding a JSONB column with a non-null default requires 2 steps in PostgreSQL: add the column with a default, then optionally drop the default if you want to enforce that new rows always provide a value. For large tables (over 100K rows), adding a column with a constant default is fast in PostgreSQL 11+ (it stores the default in the catalog, not on each row). Backfilling a new JSON field from existing columns requires a migration that reads or computes the value. queryInterface.bulkUpdate() can set a JSON default for all rows, but for computed values from multiple columns, write a raw SQL UPDATE statement with json_build_object() or jsonb_build_object(). Always test backfill migrations in a transaction with a rollback path — JSON column migrations are not easily reversible once live data has been written in the new shape.

// --- Migration: add a JSONB column with a NOT NULL default ---
// File: migrations/20260527-add-metadata-to-users.js
"use strict"

const { DataTypes } = require("sequelize")

module.exports = {
  async up(queryInterface) {
    await queryInterface.addColumn("users", "metadata", {
      type:         DataTypes.JSONB,
      allowNull:    false,
      defaultValue: {},   // column-level default: empty JSON object
    })
    // Generated SQL:
    // ALTER TABLE users ADD COLUMN metadata jsonb NOT NULL DEFAULT '{}';
  },

  async down(queryInterface) {
    await queryInterface.removeColumn("users", "metadata")
  },
}

// --- Migration: backfill a new JSON column from existing columns ---
// Suppose users has firstName and lastName columns that we want to merge into metadata
module.exports = {
  async up(queryInterface) {
    // Step 1: add the nullable column first
    await queryInterface.addColumn("users", "profile", {
      type:      DataTypes.JSONB,
      allowNull: true,
    })

    // Step 2: backfill from existing columns using jsonb_build_object
    await queryInterface.sequelize.query(
      "UPDATE users SET profile = jsonb_build_object('firstName', "firstName", 'lastName', "lastName")"
    )

    // Step 3: make NOT NULL after backfill
    await queryInterface.changeColumn("users", "profile", {
      type:      DataTypes.JSONB,
      allowNull: false,
    })
  },

  async down(queryInterface) {
    await queryInterface.removeColumn("users", "profile")
  },
}

// --- Migration: rename a key in an existing JSONB column ---
module.exports = {
  async up(queryInterface) {
    // Remove old key "userName", add new key "username" with the same value
    await queryInterface.sequelize.query(
      "UPDATE users SET metadata = (metadata - 'userName') || jsonb_build_object('username', metadata->>'userName')"
      + " WHERE metadata ? 'userName'"
    )
  },

  async down(queryInterface) {
    await queryInterface.sequelize.query(
      "UPDATE users SET metadata = (metadata - 'username') || jsonb_build_object('userName', metadata->>'username')"
      + " WHERE metadata ? 'username'"
    )
  },
}

Definitions

DataTypes.JSONB
Sequelize column type that maps to PostgreSQL's binary jsonb type. Stores a pre-parsed binary representation of JSON — faster to query than DataTypes.JSON, supports GIN indexing, and enables the @> containment and ? existence operators. Only available for PostgreSQL.
DataTypes.JSON
Sequelize column type that maps to the JSON column type in MySQL, MariaDB, SQLite, and PostgreSQL. Stores JSON as text. In PostgreSQL, it re-parses the text on every read and does not support GIN indexes. In MySQL, it stores binary-optimized JSON natively. Use DataTypes.JSONB instead for PostgreSQL.
Op.contains
Sequelize operator that maps to PostgreSQL's @> JSONB containment operator. Used in a where clause as { [Op.contains]: { key: "value" } }. Only works with JSONB columns in PostgreSQL — not supported in MySQL or with DataTypes.JSON.
jsonb_set()
PostgreSQL function that returns a copy of a JSONB value with a specific path replaced or created. Signature: jsonb_set(target jsonb, path text[], new_value jsonb, create_if_missing boolean). Used with Sequelize.literal() for atomic in-place JSON field updates without a read-then-write round-trip.
GIN index
Generalized Inverted Index — a PostgreSQL index type that decomposes composite values (like JSONB documents) into their elements and builds an inverted map. Reduces JSONB containment query time from O(n) to O(log n). Created in Sequelize migrations with queryInterface.addIndex("table", ["col"], { using: "gin" }).
Sequelize.literal()
Sequelize escape hatch that passes a raw SQL string through to the database driver without escaping. Used for JSON path operators (->, ->> ), PostgreSQL functions (jsonb_set, jsonb_build_object), and any operator not abstracted in Sequelize's Op set.

FAQ

How do I define a JSON column in Sequelize?

Use DataTypes.JSONB for PostgreSQL or DataTypes.JSON for MySQL, MariaDB, and SQLite. In the model attribute definition, set type: DataTypes.JSONB. Sequelize automatically calls JSON.stringify before writing to the database and JSON.parse when reading — you always work with plain JavaScript objects. Set allowNull: false and defaultValue: {} for a non-nullable column with an empty object default. DataTypes.JSONB (PostgreSQL only) stores binary-decomposed JSON and supports GIN indexes and @> containment. DataTypes.JSON stores a text copy and re-parses on every read.

How do I query a nested JSON field in Sequelize?

Use Sequelize.literal() for raw SQL path expressions. For PostgreSQL JSONB text extraction: where(literal("metadata->>'plan'"), 'pro'). For MySQL: where(literal("JSON_EXTRACT(metadata, '$.plan')"), 'pro'). For JSONB containment without literal(), use Op.contains: { metadata: { [Op.contains]: { plan: 'pro' } } }. To order by a JSON field: order: [[literal("metadata->>'score'"), 'DESC']]. Chain PostgreSQL operators for nested paths: literal("metadata->'address'->>'city'").

What is the difference between DataTypes.JSON and DataTypes.JSONB in Sequelize?

DataTypes.JSON maps to the text-stored json type in PostgreSQL, or the native JSON column in MySQL. DataTypes.JSONB maps to PostgreSQL's binary jsonb type and is only available in PostgreSQL. DataTypes.JSONB is 10–30% faster for path queries, supports GIN indexing, and supports @> containment and ? existence operators. DataTypes.JSON preserves key order and whitespace. For new PostgreSQL applications, always use DataTypes.JSONB unless you need byte-for-byte fidelity.

How do I add TypeScript types to a Sequelize JSON column?

In a class-based model with InferAttributes, add a typed declare field: declare metadata: ProductMetadata. This overrides the default object type and gives compile-time checking on inserts and reads. With sequelize-typescript, use the @Column({ type: DataType.JSONB }) decorator on a typed field declaration. TypeScript types are compile-time only — Sequelize does not validate the JSON shape at runtime. Add class-validator decorators or explicit schema validation in a beforeCreate/beforeUpdate hook for runtime safety.

How do I create a GIN index on a jsonb column in Sequelize?

Use a raw migration with queryInterface.addIndex("users", ["metadata"], { name: "idx_gin", using: "gin" }). This generates CREATE INDEX idx_gin ON users USING gin(metadata). For a jsonb_path_ops GIN (containment-only, 20–40% smaller), use queryInterface.sequelize.query("CREATE INDEX ... USING gin(metadata jsonb_path_ops)") — Sequelize's addIndex doesn't support operator class syntax. On a 1M row table, a GIN index reduces containment query time from ~800ms to ~4ms.

How do I safely update a nested JSON value in Sequelize?

The fetch-mutate-save pattern (findByPk → spread → save()) is simple but risks race conditions. Add optimistic locking by setting { version: true } in model options — Sequelize manages a version integer column and throws OptimisticLockError on concurrent saves. For atomic single-round-trip updates, use Model.update({ metadata: sequelize.literal("jsonb_set(metadata, '{plan}', '\"pro\"'::jsonb)") }, { where: { id: 1 } }). This is the safest pattern for high-concurrency JSON field updates because the entire read-modify-write happens atomically inside PostgreSQL.

Validate your Sequelize JSON data

Paste a JSON object from a Sequelize query result into Jsonic to validate the structure before writing your TypeScript interface declaration.

Open JSON Validator

Further reading and primary sources

  • Sequelize DataTypes documentationOfficial Sequelize reference for all data types including DataTypes.JSON and DataTypes.JSONB, their supported dialects, and configuration options
  • Sequelize querying — operatorsOfficial Sequelize documentation for Op operators including Op.contains, Sequelize.where(), Sequelize.col(), and Sequelize.literal() for raw expressions
  • Sequelize TypeScript supportOfficial guide for typing Sequelize models with InferAttributes, InferCreationAttributes, declare field declarations, and CreationOptional
  • PostgreSQL JSONB operatorsPostgreSQL official documentation for all JSON and JSONB operators: ->, ->>, @>, <@, ?, ?|, ?&, jsonb_set(), jsonb_build_object(), and path functions
  • Sequelize migrationsOfficial Sequelize migration reference: queryInterface.addColumn, addIndex, changeColumn, bulkUpdate, and running migrations with sequelize-cli