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
- Ensure the values argument is a plain object mapping column names to values
- Parse and shape request input before calling write APIs (e.g. Object.fromEntries(formData))
- 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
- Parse and shape request bodies before passing them to write APIs
- Use isPlainObject-style guards on untrusted input
- Type the values parameter as Record<string, unknown> at the boundary
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
- upsert requires at least one value
- insertMany() requires at least one explicit value across the
- update() requires at least one change
- Invalid afterRead callback result for table "' + tableName +
- Invalid beforeDelete callback result for table "' + context.
AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27).
Data as JSON: /api/errors/d6c6116907893eff.
Report an issue: GitHub.