Quick answer: The Content-Type for JSON is
application/json, registered in RFC 8259.text/jsonis not a registered media type and should not be used. Addingcharset=utf-8is allowed but has no effect, because JSON is always UTF-8. Specialized formats use a+jsonsuffix, such asapplication/ld+jsonorapplication/problem+json.
You send a perfectly valid JSON body to an API and get back 415 Unsupported Media Type. Or your Express handler logs req.body as {} even though the client swears it sent data. Nine times out of ten the JSON is fine and the header is wrong. This post is the reference for getting that header right on both ends.
Explore a valid sample in the JSON Viewer to check its fields and nesting. The viewer does not perform the protocol-specific validation discussed in this guide.
application/json Is the Only Standard JSON Content-Type
RFC 8259, the JSON specification, registers exactly one media type: application/json. The registration lists no required or optional parameters. That is unusual; most text formats define a charset. JSON does not need one because section 8.1 of the same RFC requires that JSON text exchanged between systems be encoded in UTF-8. The encoding is part of the format, not a negotiable detail.
So the minimal, fully correct header is:
Content-Type: application/json
Gzip does not change that media type: it adds a separate Content-Encoding contract. If a response becomes unreadable after a proxy or middleware change, follow the JSON API compression checklist to compare the headers with the bytes and identify which layer decodes them.
If your framework emits application/json; charset=utf-8 instead, leave it. The RFC says the parameter has no effect on compliant receivers, and a few very old HTTP clients used it as a hint. It is noise, not an error. What you should never do is label JSON with a different charset such as iso-8859-1; the bytes must be UTF-8 regardless, and the mismatch confuses debugging tools. If you are seeing mangled accented characters, the cause is usually a byte order mark or a mis-encoded file rather than the header, which our JSON encoding and BOM guide covers.
Why text/json Is Wrong
text/json looks plausible because text/html, text/plain, and text/css exist. But it was never submitted to IANA and appears in no specification. It survives because some early web frameworks guessed at it and because a handful of enterprise gateways still pass it through.
Three practical reasons to stop using it:
- Strict body parsers skip it. Express's
express.json()only parses requests whose type matchesapplication/jsonby default, so atext/jsonbody is silently ignored andreq.bodystays empty. - The
text/*tree implies a default charset of US-ASCII under older HTTP rules, which contradicts the UTF-8 requirement for JSON. - Content negotiation breaks. A client that sends
Accept: application/jsoncan legitimately be told406 Not Acceptableby a server that only knows how to producetext/json.
The +json Family: ld+json, problem+json, vnd.api+json
RFC 6839 defines +json as a structured syntax suffix. Any media type ending in it promises "this body is valid JSON, and it also follows a more specific format." A generic JSON parser can read it; a specialized client can do more with it.
| Media type | Format | Defined by | Where you see it |
|---|---|---|---|
application/json | Plain JSON | RFC 8259 | Every REST API |
application/ld+json | JSON-LD linked data | W3C JSON-LD | Structured data in HTML, semantic web |
application/problem+json | Problem Details error body | RFC 9457 | Standardized API error responses |
application/vnd.api+json | JSON:API documents | jsonapi.org | APIs that follow the JSON:API spec |
application/json-patch+json | JSON Patch operations | RFC 6902 | HTTP PATCH request bodies |
application/merge-patch+json | JSON Merge Patch | RFC 7386 | HTTP PATCH request bodies |
application/geo+json | GeoJSON | RFC 7946 | Maps and geographic features |
text/json | Not registered | None | Legacy servers; avoid |
Two of these come up constantly. application/ld+json is the type on the <script> tag that carries search engine structured data; see JSON-LD for SEO for working examples. application/problem+json is the type for standardized error bodies, which we cover in JSON error response formats. If your server accepts PATCH requests with these suffixed types, remember to widen the body parser:
// Express: also parse application/problem+json, application/merge-patch+json, etc.
app.use(express.json({ type: ["application/json", "application/*+json"] }));
Setting the JSON Content-Type in Each Framework
Every mainstream framework sets the header for you when you use its JSON response helper. The mistakes happen when people bypass the helper and write a string by hand.
| Framework | Helper | Header it sends |
|---|---|---|
| Express | res.json(data) | application/json; charset=utf-8 |
| Flask | return jsonify(data) | application/json |
| Django | return JsonResponse(data) | application/json |
| Spring Boot | @RestController returning an object | application/json |
| Next.js Route Handler | NextResponse.json(data) | application/json |
| FastAPI | return data from a path function | application/json |
| Laravel | return response()->json($data) (see Laravel JSON API responses) | application/json |
| Plain PHP | header('Content-Type: application/json'); echo json_encode($data); (see json_encode vs json_decode) | application/json |
The wrong way and the right way side by side in Express:
// Wrong: header defaults to text/html
app.get("/users/42", (req, res) => {
res.send(JSON.stringify({ id: 42, name: "Priya" }));
});
// Right: serializes and sets Content-Type: application/json; charset=utf-8
app.get("/users/42", (req, res) => {
res.json({ id: 42, name: "Priya" });
});
Flask and Django have the same split:
# Flask
from flask import jsonify
@app.get("/users/<int:user_id>")
def get_user(user_id):
return jsonify(id=user_id, name="Priya") # application/json
# Django
from django.http import JsonResponse
def get_user(request, user_id):
return JsonResponse({"id": user_id, "name": "Priya"})
In a Next.js App Router route handler, NextResponse.json() (or the standard Response.json()) does the same job:
// app/api/users/[id]/route.js
import { NextResponse } from "next/server";
export async function GET(request, { params }) {
const { id } = await params;
return NextResponse.json({ id, name: "Priya" });
}
Spring Boot picks the type from the Accept header and the available converters; with Jackson on the classpath, a @RestController method returning an object produces application/json. To pin it explicitly use @GetMapping(value = "/users/{id}", produces = MediaType.APPLICATION_JSON_VALUE).
For static files served by nginx, the bundled mime.types file already maps the .json extension to application/json. If you serve JSON from a location with no extension, set the type directly:
location /api/config {
default_type application/json;
alias /var/www/config.json;
}
Content-Type on Requests: Where 415 Comes From
The header matters just as much in the other direction. A 415 Unsupported Media Type response means the server looked at your request's Content-Type and refused the body before parsing it. Spring is explicit about it:
HTTP/1.1 415 Unsupported Media Type
{"status":415,"error":"Unsupported Media Type",
"message":"Content-Type 'text/plain;charset=UTF-8' is not supported"}
The usual causes, in order of how often we see them:
-
fetch with a string body and no header. The Fetch standard sets
text/plain;charset=UTF-8when you pass a string. Add the header yourself:fetch("/api/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "Priya" }), });More patterns in fetch JSON in JavaScript.
-
curl with -d and no header. curl defaults to
application/x-www-form-urlencoded. Pass-H "Content-Type: application/json"; the full set of flags is in how to send JSON with curl. -
Passing an object instead of a string.
body: { name: "Priya" }in fetch becomes the literal text[object Object]. The header may be right, but the body is not JSON. -
A proxy rewriting the header. Some API gateways normalize unknown types. Check the request as the server sees it, not as the client sent it.
Express behaves differently: instead of a 415 it silently leaves req.body as an empty object when the type does not match. If a handler reads undefined from every field, check the request header before you check the JSON.
Accept Headers and Content Negotiation
Accept is the client saying what it wants back. A well-behaved JSON client sends Accept: application/json, and a well-behaved server honours it or returns 406 Not Acceptable. In practice most JSON-only APIs ignore Accept and always return JSON, which is fine as long as they still label the response correctly.
Negotiation becomes useful when one endpoint can speak several formats. In Express, req.accepts(["json", "html"]) returns the best match so you can render a page for browsers and JSON for scripts from the same route. Spring does the same automatically based on registered message converters. Our guide to JSON in REST APIs shows where this fits in a full request/response design.
What Browsers Do With application/json
Opening a JSON URL directly in a browser does not trigger a download. Firefox renders application/json in a built-in viewer with collapsible nodes and a raw tab; Chrome and Edge show it as plain text. A download happens only when the server adds Content-Disposition: attachment or uses a type the browser cannot display, such as application/octet-stream. If a browser is downloading your .json files, that is the header to look at.
When a response renders as a wall of unformatted text, copy it into JSON Prettifier to format and validate it in the browser. That is also the quickest way to confirm whether an unexpected response is JSON at all; an HTML login page labelled text/html will fail validation on the first character, which is the classic unexpected token < error.
Common Mistakes
- Setting the header after the body. In Node's raw
httpmodule, headers sent afterres.write()are ignored. Callres.setHeader()first. - Returning JSON with text/html on error pages. Error handlers often fall back to the framework default. Make the error path use the same JSON helper as the success path.
- Using application/javascript for JSON. That type is for executable scripts. JSON embedded in HTML for a script to read belongs in
<script type="application/json">, which the browser will not execute. - Assuming charset fixes encoding. If the bytes are Latin-1, declaring UTF-8 makes it worse. Fix the source encoding.