remix-run/remix · error · DataTableValidationError

Invalid value for table "' + tableName + '"

Error message

Invalid value for table "' + tableName + '"

What it means

normalizeWriteObject requires each written value to be a plain object (not null, not an array). If the value passed to insert/update/upsert is not an object, a DataTableValidationError with the message 'Invalid value for table ... Expected object' is thrown before any hook or SQL runs.

Source

Thrown at packages/data-table/src/lib/database/write-lifecycle.ts:361

function hasIssues(value: unknown): value is { issues: ReadonlyArray<ValidationIssue> } {
  return typeof value === 'object' && value !== null && 'issues' in value
}

function hasValue(value: unknown): value is { value: unknown } {
  return typeof value === 'object' && value !== null && 'value' in value
}

function normalizeWriteObject<table extends AnyTable>(
  table: table,
  value: unknown,
  operation: TableWriteOperation,
  source?: LifecycleCallbackSource,
): Record<string, unknown> {
  let tableName = getTableName(table)
  let columns = getTableColumns(table)

  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
    throw new DataTableValidationError(
      'Invalid value for table "' + tableName + '"',
      [{ message: 'Expected object' }],
      {
        metadata: {
          table: tableName,
          operation,
          ...(source ? { source } : {}),
        },
      },
    )
  }

  let output: Record<string, unknown> = {}

  for (let key in value) {
    if (!Object.prototype.hasOwnProperty.call(value, key)) {
      continue
    }

View on GitHub (pinned to 9696913134)

Solutions

  1. Ensure the values argument is a plain object mapping column names to values
  2. Parse and shape request input before calling write APIs (e.g. Object.fromEntries(formData))
  3. Wrap untrusted input in a validator that guarantees an object shape

Example fix

// before
await table.create({ values: req.body }) // body is a string

// after
let values = JSON.parse(req.body)
await table.create({ values })
Defensive patterns

Strategy: type-guard

Validate before calling

if (typeof values !== 'object' || values === null || Array.isArray(values)) {
  throw new Error('values must be a plain object')
}

Type guard

function isPlainObject(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null && !Array.isArray(value)
}

Try / catch

try {
  await table.create({ values })
} catch (error) {
  if (error instanceof DataTableValidationError && /Expected object/.test(error.message)) {
    return new Response('Invalid body', { status: 400 })
  } else throw error
}

Prevention

When it happens

Trigger: Passing a string, number, null, or array as the values/changes argument, e.g. create({ values: JSON.parse(body) }) where the body parses to a non-object; passing an array to a single-row API.

Common situations: Parsing untyped request JSON and forwarding it directly; passing FormData or URLSearchParams without conversion; accidentally passing rows[0].someField (a string) instead of rows[0].

Related errors


AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27). Data as JSON: /api/errors/d6c6116907893eff. Report an issue: GitHub.