JSON Empty String vs Null: Semantic Differences, Database Behavior, and API Design
Quick answer
💡Use null when a field has no value — it was never provided or is unknown. Use empty string when a field exists with explicitly no text content. Both are falsy in JavaScript but differ in SQL (NULL fails IS NOT NULL; empty string does not), in TypeScript types (string | null vs string), and in JSON Schema validation. Define which is valid for each field in your API contract and enforce it consistently.
Error symptoms
- ✕
COALESCE(displayName, 'Anonymous') returns 'Anonymous' for null but not for empty string, causing inconsistent display names - ✕
WHERE name IS NULL misses rows where name is an empty string, returning incomplete result sets - ✕
TypeScript error: Type 'null' is not assignable to type 'string' when a nullable field from an API is used directly - ✕
A form that clears a field sends empty string, but the API rejects it expecting null for optional fields - ✕
if (profileName) check treats empty string and null identically in JavaScript, hiding a semantic distinction - ✕
JSON Schema validation fails because the field type is 'string' but the API sends null for unset values
Common causes
- •Different engineers on the same team use null and empty string interchangeably for optional text fields without a shared convention
- •HTML forms always submit empty strings for unfilled text inputs — they cannot represent null natively
- •ORM defaults vary: some set missing fields to null, others to empty string, depending on the column definition
- •Legacy APIs that predate REST conventions use empty strings where modern APIs would use null or omit the field entirely
- •Database schema allows both: a VARCHAR column that is NULLABLE and has no DEFAULT '' constraint can store either
- •GraphQL and JSON Schema define these differently, causing mismatch when one service generates the schema and another consumes it
When it happens
- •A user profile page shows 'null' as literal text because the template renders the raw null value instead of a placeholder
- •A form's clear button sets a field to empty string but the API contract specifies null for cleared fields and rejects the update
- •A SQL query using COALESCE to provide fallback values only handles null but not empty string, giving inconsistent results
- •A TypeScript function typed to accept string throws a type error when passed a null value from a parsed JSON response
- •A JSON Schema with type: 'string' rejects a null value, causing validation failures for fields the API legitimately leaves unset
Examples and fixes
A user profile table stores the displayName column with mixed null and empty string values. A query using COALESCE to provide a fallback value produces inconsistent results depending on which sentinel value was used.
SQL COALESCE behaves differently for null versus empty string
❌ Wrong
-- Some rows have displayName = NULL
-- Some rows have displayName = '' (empty string)
-- Both are meant to represent 'no display name set'
SELECT
userId,
COALESCE(displayName, 'Anonymous') AS resolvedName
FROM userProfiles
WHERE userId IN (101, 102, 103);
-- userId 101: displayName = NULL -> resolvedName = 'Anonymous' OK
-- userId 102: displayName = '' -> resolvedName = '' WRONG
-- userId 103: displayName = 'Eve' -> resolvedName = 'Eve' OK✅ Fixed
-- Normalize both null and empty string to the fallback value
SELECT
userId,
COALESCE(NULLIF(displayName, ''), 'Anonymous') AS resolvedName
FROM userProfiles
WHERE userId IN (101, 102, 103);
-- NULLIF(displayName, '') converts '' to NULL before COALESCE runs
-- userId 101: NULL -> NULLIF -> NULL -> COALESCE -> 'Anonymous'
-- userId 102: '' -> NULLIF -> NULL -> COALESCE -> 'Anonymous'
-- userId 103: 'Eve' -> NULLIF -> 'Eve' -> COALESCE -> 'Eve'COALESCE returns the first non-null argument. An empty string is not null — it is a valid string value — so COALESCE passes it through unchanged. The NULLIF(displayName, '') function converts an empty string to NULL before COALESCE evaluates it, making both null and empty string equivalent for the purpose of fallback display logic. This is the standard SQL pattern when a column was designed without a clear null versus empty string convention and the data contains a mix of both sentinels.
A profile editing form sends a JSON payload when a user clears the nickname field. The API expects null for cleared optional fields but receives an empty string, which fails schema validation.
API form submission sending empty string instead of null
❌ Wrong
// HTML form: <input name="nickname" value="" />
// Submitting the form produces an empty string for cleared fields
const formData = new FormData(profileForm);
const payload = {
userId: formData.get('userId'),
nickname: formData.get('nickname'), // '' — empty string from form
bio: formData.get('bio')
};
await fetch('/api/profile', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
// Server rejects: { error: 'nickname must be null or a non-empty string (min 2 chars)' }✅ Fixed
// Normalize empty strings from form inputs to null before sending
function normalizeFormField(value) {
if (value === null || value === undefined) return null;
const trimmed = String(value).trim();
return trimmed.length === 0 ? null : trimmed;
}
const formData = new FormData(profileForm);
const payload = {
userId: formData.get('userId'),
nickname: normalizeFormField(formData.get('nickname')),
bio: normalizeFormField(formData.get('bio'))
};
await fetch('/api/profile', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
// Server receives: { userId: '...', nickname: null, bio: null }HTML form inputs always produce strings — an empty input produces an empty string, never null. The gap between HTML's representation and the API's expected contract must be bridged explicitly in the form submission handler. The normalizeFormField helper trims whitespace and converts any resulting empty string to null, satisfying the API's requirement that optional text fields be null rather than empty when cleared. This normalization belongs at the form submission layer, not scattered throughout the form validation logic.
Null signals absence; empty string signals clearance
In JSON, null and empty string are two entirely distinct values that happen to both be falsy in JavaScript. Their semantic meaning is different and that difference matters at every layer of a system. Null means the field has no value — it was never provided, it is unknown, or it is not applicable in the current context. An empty string means the field was explicitly set to text content that contains no characters. The difference is subtle but consequential.
Consider a user profile with a displayName field. When a new user creates an account without entering a display name, the field is null — it was never provided. When an existing user had a display name of 'Alex' and deliberately cleared it, the field is now an empty string — it was explicitly set to nothing. Both produce the same rendering problem in a naive template that just outputs the value, but they have different meanings: null means 'we don't know', empty string means 'they cleared it on purpose'.
This semantic difference matters enormously in databases. In SQL, NULL is a special marker that propagates through comparisons in ways that empty string does not. A query with WHERE displayName = '' returns only rows with an empty string; it does not return rows where displayName IS NULL. Conversely, WHERE displayName IS NULL returns only null rows; it does not return empty string rows. The two values are completely invisible to each other's queries unless you explicitly handle both with IS NULL OR displayName = '' or the NULLIF/COALESCE pattern.
In TypeScript, the difference creates two distinct type contracts. A function typed as profileName: string can never legally receive null — the type system treats null as a different type from string. A function typed as profileName: string | null must handle both cases explicitly. These different type signatures communicate intent to callers: string means the field is always present and never null; string | null means the caller must handle the absent case. This is information that is lost when null and empty string are used interchangeably without a documented convention.
API contract design should make this decision explicitly for every optional text field. A field that represents user-provided text that may not yet have been entered should use null for the not-yet-entered state and a non-empty string for any provided value. A field that might be intentionally set to empty — like a bio that a user wants to remove — is a different case and may legitimately use empty string to signal the intentional clearing. Documenting this per-field in your OpenAPI specification prevents the ambiguity from compounding as the system grows.
Tracing empty-string versus null through the stack
When a bug involves unexpected null or empty string values, trace the value through every layer that touches it: the client form, the HTTP request body, the API deserialization layer, the database write, the database read, the API serialization layer, and finally the client rendering. The value can change at any of these boundaries, and finding where the mismatch is introduced is faster than guessing.
For the client form boundary, log the raw FormData or the request body before it is sent. In JavaScript, you can read a FormData entry with formData.get('fieldName') and log its type and value. A text input that appears empty in the UI always produces an empty string here, never null. If you expect null at this point, you have found the gap — a normalization step is needed between reading the FormData and building the request payload.
For the API boundary, log the raw parsed request body after deserialization. In Express, console.log(req.body.fieldName, typeof req.body.fieldName) immediately shows whether the field arrived as null, as an empty string, or as undefined (field was omitted entirely). These three are different: null means the client sent the JSON literal null, empty string means the client sent the JSON string "", and undefined means the field key was not present in the JSON at all.
For the database boundary, run the SQL query directly against the database to inspect what is actually stored. SELECT userId, displayName, displayName IS NULL AS isNull, displayName = '' AS isEmpty FROM userProfiles WHERE userId = 123 shows the stored value and its type distinction in a single query. This eliminates any uncertainty about whether the ORM or serialization layer is transforming the value between the database and the application code.
For the rendering boundary, check whether the template uses a strict null check or a falsy check. In JavaScript templates, both null and empty string fail an if (profileName) check, but they serialize differently to JSON and render differently to strings. Logging typeof displayedValue and the value itself just before rendering identifies whether the template receives null or empty string and where that comes from.
Database NULL handling with COALESCE and IS NULL
In SQL, the most common operation for handling null values with a fallback is COALESCE. COALESCE accepts any number of arguments and returns the first non-null value. COALESCE(displayName, 'Anonymous') returns 'Anonymous' when displayName is null, and returns the displayName value otherwise. The crucial limitation is that COALESCE does not treat empty string as equivalent to null — an empty string passes through COALESCE unchanged.
The standard SQL pattern for normalizing both null and empty string to a fallback value combines NULLIF and COALESCE. NULLIF(expression, '') converts an empty string to null and leaves all other values unchanged. Wrapping that in COALESCE(NULLIF(displayName, ''), 'Anonymous') handles both cases: null passes through NULLIF unchanged and then triggers COALESCE's fallback; empty string is converted to null by NULLIF and then also triggers COALESCE's fallback.
For PostgreSQL specifically, the NULLIF approach is idiomatic and widely understood. For MySQL and SQLite, the same syntax works. In application-level ORMs, you can apply the same normalization: in Prisma, a custom field transformer at the schema level can convert empty strings to null on write; in TypeORM, an entity lifecycle hook can apply the normalization before insert and update operations.
For API responses, the normalization direction is typically reversed — the database stores null and the API should decide what to send. Sending null in the JSON response is straightforward since JSON.stringify serializes null as the literal null. If the API contract specifies that the field is omitted entirely when null rather than sent as null, use a replacer function with JSON.stringify or build the response object conditionally: if (profileName !== null) { responseBody.profileName = profileName; }. Omitting the key and sending null are both valid approaches, but the choice must be documented and consistent.
TypeScript string-or-null versus optional fields
TypeScript distinguishes three distinct cases that developers often conflate when working with JSON data: a field with type string, a field with type string | null, and an optional field typed as string | undefined using the ? modifier. These are not interchangeable and they have different implications for JSON serialization.
A field typed as string in a TypeScript interface is guaranteed to be a non-null, non-undefined string. TypeScript will flag any code that tries to pass null or undefined to this field as a type error. This corresponds to a JSON Schema field with type: 'string' and no null allowed — the field must always be present and must always contain a string value.
A field typed as string | null explicitly allows null as a valid value alongside string. In JSON, this field appears as either a quoted string or the literal null. In JSON Schema, this corresponds to type: ['string', 'null'] or in JSON Schema draft 2020-12, the nullable construct. When TypeScript code accesses this field, the compiler requires a null check before using string methods — accessing profileName.trim() without first confirming profileName !== null is a type error.
An optional field using the ? modifier — typed as string | undefined — means the field may be absent from the object entirely. TypeScript uses undefined rather than null for absent properties, which is a JavaScript convention. However, JSON.stringify treats these differently: undefined property values are omitted from the serialized JSON, while null values are written as the literal null. This means an object with an optional string? field serializes to JSON without that key when the value is undefined, but the same object with a string | null field serializes with a null value when it is null. These are different JSON structures and the API must document which behavior it expects.
For API payloads, the cleanest convention is to use null for fields that are present but have no value, and to omit the field entirely for fields that are not applicable. This avoids the undefined versus null ambiguity in TypeScript and produces clean JSON responses. Document the distinction explicitly in your OpenAPI schema using nullable: true for nullable fields and required: false for optional fields, as these are semantically different modifiers.
When form inputs submit empty strings the API rejects
HTML form inputs are the most common source of accidental empty strings in JSON payloads. When a user leaves a text input blank and submits the form, the browser produces an empty string for that field. JavaScript's FormData API, URLSearchParams, and HTML form serialization all follow this behavior — there is no native mechanism in HTML for a form to produce a JSON null value.
This creates a mismatch whenever the API contract specifies null for absent optional fields. The form sends empty string, the API validates the field and rejects it because the field value is not null as required, and the user sees a generic validation error that gives no indication of the actual cause. The fix is a normalization function in the form submission handler that converts empty strings to null before building the request payload.
An even subtler variant involves trimming. A user types a single space into a text field and submits. The raw form value is ' ' — a non-empty string containing one space character. A strict type validator passes it as a valid string. But after trimming, it becomes an empty string that should be treated as absent. Normalization functions should trim before checking for empty: const normalizedValue = rawValue.trim().length === 0 ? null : rawValue.trim(). Doing the trim once and reusing the result avoids trimming twice.
Input normalization at the form submission layer is preferable to input normalization at the server. If the server silently converts empty strings to null, client developers do not learn that the API expects null, and the implicit conversion becomes an undocumented contract that breaks when the normalization is removed or changed. Better to be explicit: document in the API spec that optional text fields must be null when absent, normalize to null in the client before sending, and return a clear validation error on the server if the client sends an empty string where null is expected.
Defining explicit null semantics in JSON schemas
The most effective way to eliminate null versus empty string ambiguity is to define the semantics explicitly in your JSON Schema and OpenAPI specification, then enforce them at the API boundary with a validator. For each optional text field, make an explicit decision: can the field be null? Can it be an empty string? Can it be both? Can it be omitted?
For a field like a user's bio that starts as absent and can be set or cleared by the user, a reasonable contract is: type ['string', 'null'] with a minLength of 1 when the type is string. This means the field is null when not set, is a non-empty string when set, and cannot be an empty string — clearing the bio sets it to null, not to empty string. In JSON Schema: {"type": ["string", "null"], "minLength": 1}. This schema rejects both null and empty string; but since the type allows null, the field can be null.
For a field that is required and always present — like a username — the contract is type: 'string' with minLength: 1 and required: true. Null is not allowed, empty string is not allowed, and the field cannot be omitted. Any code that tries to send null for this field fails schema validation immediately, preventing the issue from reaching the database layer.
In GraphQL, the distinction maps to the type system directly. A field declared as String! is required and non-null. A field declared as String is nullable — it may be null. GraphQL has no native concept of empty string as a special value, so the semantic convention for empty string must be documented in the schema description. The choice of whether cleared fields should be null or omitted is a GraphQL API design decision that should be consistent across all fields of the same conceptual type.
For backward compatibility during API migrations, if an existing API used empty string where null would be more correct, add a deprecation notice in the OpenAPI schema for the empty string case, continue accepting both during a transition period, and document the migration timeline. Changing null versus empty string behavior is a breaking change for any client that checks the type explicitly, so migration must be coordinated across client and server teams with advance notice.
Quick fix checklist
- ✓Define explicitly in the API spec whether each optional text field should be null or omitted when not set — never allow both interchangeably
- ✓Add a normalization function in form submission handlers that converts empty strings to null before building the request payload
- ✓Use COALESCE(NULLIF(columnName, ''), 'fallback') in SQL to handle both null and empty string as equivalent absent values
- ✓Define TypeScript types as string | null for nullable fields and enforce null checks before calling string methods
- ✓Add type: ['string', 'null'] and minLength: 1 in JSON Schema to disallow empty strings while allowing null for cleared fields
- ✓Run SELECT IS NULL, = '' checks directly in the database when debugging unexpected COALESCE behavior
- ✓Normalize trimmed empty strings to null — a single space after trimming is semantically the same as no input
- ✓Check the raw req.body value in the API log before database writes to confirm whether null or empty string arrived from the client
Related guides
Frequently asked questions
What is the difference between null and empty string in JSON?
In JSON, null is the literal null value representing absence or no value. An empty string '' is a valid string with zero characters. Both are falsy in JavaScript, but they are entirely different types with different behaviors in SQL WHERE clauses, JSON Schema validators, TypeScript type checks, and API contracts. Always define which you intend for each optional field and enforce it consistently.
Should I use null or empty string for an optional API field?
Use null when the field has no value — it was not provided, not applicable, or unknown. Use empty string only when the field exists with deliberately no text content, which is uncommon for most API fields. Null is the conventional representation of 'not set' in JSON APIs. Sending null is cleaner and more compatible with SQL's IS NULL checks and JSON Schema's nullable pattern.
Why does COALESCE not work on empty strings?
COALESCE returns the first non-null argument. An empty string is not null — it is a valid string value — so COALESCE passes it through without substituting the fallback. Use COALESCE(NULLIF(columnName, ''), 'fallback') to handle both null and empty string. NULLIF converts the empty string to null first, and then COALESCE substitutes the fallback for the resulting null.
How do I make TypeScript accept both null and empty string for a field?
Type the field as string | null to allow null explicitly. If empty string is also valid, type it as string | null and document the distinction in comments. For a field that should never be empty string but may be null, add a runtime validation check that rejects empty strings: if (value !== null && value.trim().length === 0) throw new Error('Field cannot be empty string'). The type system alone does not enforce minimum length.
Why does HTML form submission send empty string instead of null?
HTML form inputs always produce string values — there is no native HTML mechanism for producing JSON null. An unfilled text input produces an empty string. To send null to a JSON API, add a normalization step in the form submission handler that checks whether the value is an empty string after trimming and replaces it with null before constructing the request payload.
How do I detect whether a database column stores null or empty string?
Run a query that checks both explicitly: SELECT columnName, columnName IS NULL AS isNull, columnName = '' AS isEmpty FROM tableName WHERE id = targetId. This shows the stored value, whether it is null, and whether it is an empty string in a single row. If both isNull and isEmpty are false, the column has a non-empty string value. Use this query to diagnose inconsistencies before applying NULLIF normalization.
Can JSON Schema validate the difference between null and empty string?
Yes. Use type: ['string', 'null'] to allow both null and string values. Add minLength: 1 to the string portion to reject empty strings while still allowing null. In JSON Schema, minLength applies only to string values and is ignored for null, so the schema {"type": ["string", "null"], "minLength": 1} accepts valid non-empty strings and null, but rejects empty string specifically.
What is the GraphQL equivalent of null versus empty string?
In GraphQL, a field declared as String! is non-nullable — it must always have a value and cannot be null. A field declared as String (without the exclamation mark) is nullable and can return null. GraphQL has no built-in concept of empty string as a distinct state from other strings — if you need to distinguish 'cleared' from 'not set', use a nullable field where null means not set and omit empty string as a valid value in the resolver logic.
How does JSON.stringify handle null and empty string differently?
JSON.stringify({name: null}) produces {"name":null} — the null is serialized as the JSON literal null. JSON.stringify({name: ''}) produces {"name":""} — the empty string is serialized as a quoted empty string. The two are visually distinct in the output. The only field value that JSON.stringify omits entirely is undefined: {name: undefined} produces {} with the name key absent from the output entirely.
All tools run in your browser. Your data never leaves your device. Last updated: 2026-05-05.