Fix Postgres 42804: Expression Is of Type Text
Chat2DB TeamAn INSERT or UPDATE that looks harmless fails with:
ERROR: column "user_id" is of type integer but expression is of type text
LINE 1: insert into events (id, user_id) select 2, '42'::text;
^
HINT: You will need to rewrite or cast the expression.The SQLSTATE is 42804 (datatype_mismatch). The wording varies with the types involved: is of type jsonb but expression is of type text, is of type boolean but expression is of type integer, is of type timestamp with time zone but expression is of type text, is of type integer but expression is of type character varying. The cause is always the same. You are storing a value whose type PostgreSQL will not convert to the column's type automatically.
What confuses people is that a very similar statement often works. INSERT ... VALUES (1, '42') succeeds while INSERT ... SELECT 2, '42'::text fails. This guide explains why, and gives the right fix for each situation. Every example was run on PostgreSQL 18.
Why one version works and the other does not
CREATE TABLE events (
id int PRIMARY KEY,
user_id int,
happened_at timestamptz,
payload jsonb,
tags text[],
active boolean
);
INSERT INTO events (id, user_id) VALUES (1, '42'); -- works
INSERT INTO events (id, user_id) SELECT 2, '42'::text; -- 42804
INSERT INTO events (id, user_id) SELECT 3, v FROM (VALUES ('42')) t(v); -- 42804In the first statement, '42' is a string literal of type unknown. PostgreSQL delays deciding its type until it sees where the value goes, then parses it directly as an integer. No conversion between types is involved.
In the second statement the value already has the type text, because of the explicit cast. In the third it is text because a column of a VALUES list is resolved to text when nothing else determines its type. Storing a text value in an integer column requires a cast from text to integer, and PostgreSQL only applies casts automatically on assignment when they are marked as assignment or implicit casts. text to integer is neither, because not every string is a number.
So the rule to remember: literals adapt to the column, typed values do not. The error appears when a value has acquired a concrete type before it reaches the column.
Which conversions happen automatically on assignment
Assignment is more permissive than comparisons or function calls. These all succeed when storing into a column:
- Numeric types into other numeric types, for example
numericintointeger(the value is rounded) orintegerintobigint. - Almost any type into
textorvarchar, through the type's output function. timestampintotimestamptzand vice versa.
These do not, and produce 42804:
textorvarcharinto numbers, dates, timestamps,jsonb,uuid,inet, enums or arrays.integerintoboolean. There is an explicit cast1::boolean, but no assignment cast.- Between unrelated types, such as
timestamptzintointeger.
Fix 1: cast the expression explicitly
The direct fix, as the HINT says, is an explicit cast on the expression:
INSERT INTO events (id, user_id) SELECT 2, '42'::text::int;
UPDATE events SET active = 1::boolean WHERE id = 1;An explicit cast can still fail at run time if the value cannot be converted, for example 'abc'::int raises invalid input syntax for type integer. That is a data problem rather than a type problem, and it is easier to diagnose because the message shows the bad value.
Fix 2: INSERT ... SELECT from a staging table
Loading data through a staging table whose columns are all text is the most common source of this error:
CREATE TABLE staging (id text, user_id text, happened_at text);
INSERT INTO staging VALUES ('9', '7', '2026-10-23 08:00:00+00');
INSERT INTO events (id, user_id, happened_at)
SELECT id, user_id, happened_at FROM staging;
-- ERROR: column "id" is of type integer but expression is of type text
-- HINT: You will need to rewrite or cast the expression.Only the first mismatched column is reported. Fix all of them at once by casting every column in the select list:
INSERT INTO events (id, user_id, happened_at)
SELECT id::int, user_id::int, happened_at::timestamptz FROM staging;If some rows contain values that do not convert, PostgreSQL 16 and newer let you test first with pg_input_is_valid('abc', 'integer'), which returns false instead of raising an error. Use it in a WHERE clause to find the bad rows before the real load.
Fix 3: application parameters sent as varchar
Drivers that send parameters with a declared type can trigger the error even though the SQL text contains no cast. The PostgreSQL JDBC driver, for instance, sends setString() values typed as varchar by default. We reproduced the effect with a prepared statement that declares the parameter type the same way:
PREPARE p(varchar) AS INSERT INTO events (id, user_id) VALUES (100, $1);
-- ERROR: column "user_id" is of type integer but expression is of type character varying
-- HINT: You will need to rewrite or cast the expression.A parameter with no declared type behaves like a literal and adapts to the column:
PREPARE p2 AS INSERT INTO events (id, user_id) VALUES (101, $1);
EXECUTE p2('5'); -- INSERT 0 1Fixes, from best to worst:
- Bind the value with the right type in the application:
setInt,setObject(..., Types.OTHER)forjsonbanduuid, or the equivalent in your ORM. - Add a cast in the SQL text:
VALUES (?, ?::int)orCAST(? AS jsonb). - For JDBC only, the connection property
stringtype=unspecifiedmakes the driver send strings untyped, so PostgreSQL infers the type from the column. It fixes many cases at once, but it also changes how ambiguous function calls resolve, so test it.
Fix 4: jsonb columns
INSERT INTO events (id, payload) SELECT 5, '{"a":1}'::text;
-- ERROR: column "payload" is of type jsonb but expression is of type textCast the text with ::jsonb, or build the value with JSON functions such as jsonb_build_object or to_jsonb. A plain literal works, because it is still unknown: UPDATE events SET payload = '{"a":1}' WHERE id = 1. In application code, ORMs usually need the column mapped as JSON so the parameter is sent with the right type, rather than as a string.
Fix 5: boolean columns and 0/1 values
Code ported from MySQL often stores 1 and 0 in boolean columns:
UPDATE events SET active = 1 WHERE id = 1;
-- ERROR: column "active" is of type boolean but expression is of type integerUse true and false, string literals such as 'yes', 't' or '1' (which are converted because they are literals), or an explicit 1::boolean. If many statements are affected, fix the generator rather than each query.
Fix 6: enum columns
CREATE TYPE mood AS ENUM ('happy', 'sad');
CREATE TABLE people (name text, m mood);
INSERT INTO people SELECT 'a', 'happy'::text;
-- ERROR: column "m" is of type mood but expression is of type text
INSERT INTO people VALUES ('a', 'happy'); -- worksCast to the enum type ('happy'::mood or value::mood). Drivers that bind enum values as strings need the same treatment as integers in Fix 3.
Fix 7: arrays from delimited strings
A comma-separated string is not an array literal. A plain literal gives a different error, and a typed text value gives 42804:
INSERT INTO events (id, tags) VALUES (8, 'a,b');
-- ERROR: malformed array literal: "a,b"
-- DETAIL: Array value must start with "{" or dimension information.
INSERT INTO events (id, tags) SELECT 9, 'a,b'::text;
-- ERROR: column "tags" is of type text[] but expression is of type textEven a correctly formatted value such as '{a,b}'::text fails the second way, because the problem is the type, not the content. Split the string with string_to_array('a,b', ','), or write a real array literal such as '{a,b}' or ARRAY['a','b'].
Related messages with the same root cause
The same type rule produces a few differently worded errors:
- Default values:
CREATE TABLE t2 (n int DEFAULT 'abc'::text)fails withcolumn "n" is of type integer but default expression is of type text. Drop the cast:DEFAULT '5'. - Changing a column type:
ALTER TABLE staging ALTER COLUMN user_id TYPE intfails withcolumn "user_id" cannot be cast automatically to type integerand the hintYou might need to specify "USING user_id::integer". Add theUSINGclause. - WHERE conditions:
SELECT * FROM events WHERE idfails withargument of WHERE must be type boolean, not type integer. Write the comparison you mean, such asWHERE id <> 0. - CASE and UNION:
CASE WHEN ... THEN id ELSE now() ENDgivesCASE types timestamp with time zone and integer cannot be matched, andSELECT id ... UNION SELECT 'x'::textgivesUNION types integer and text cannot be matched. All branches must share a type, so cast them to a common one, usuallytext.
A related trap is CASE WHEN id > 1 THEN id ELSE 'none' END. Here 'none' is an untyped literal, so PostgreSQL tries to read it as an integer and fails with invalid input syntax for type integer: "none". Cast the other branch instead: THEN id::text ELSE 'none'. The same applies to coalesce(user_id, 'unknown').
Finding the column types quickly
When a statement inserts into many columns, it helps to see the target types side by side:
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = 'events'
ORDER BY ordinal_position;In Chat2DB (opens in a new tab), the table structure is shown next to the editor, and the AI assistant can take a failing INSERT ... SELECT and add the casts each column needs, based on the real types of both tables. That is especially helpful for wide staging tables, where PostgreSQL reports only the first mismatched column at a time.
Summary
column "x" is of type A but expression is of type B means a value with a definite type is being stored in a column of another type, and no automatic assignment cast exists between them. String literals are not affected because they have no type until they reach the column; typed values, such as columns from a text staging table, varchar parameters from a driver, or anything with an explicit ::text, are. Cast the expression to the column's type, bind application parameters with the correct type, and use USING when changing a column's type.
