Skip to content
APIs

application/json: The Correct JSON Content-Type and Why text/json Is Wrong

·6 min read
application/json: The Correct JSON Content-Type and Why text/json Is Wrong - APIs guide from JSON Prettifier

Quick answer: The Content-Type for JSON is application/json, registered in RFC 8259. text/json is not a registered media type and should not be used. Adding charset=utf-8 is allowed but has no effect, because JSON is always UTF-8. Specialized formats use a +json suffix, such as application/ld+json or application/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 matches application/json by default, so a text/json body is silently ignored and req.body stays 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/json can legitimately be told 406 Not Acceptable by a server that only knows how to produce text/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 typeFormatDefined byWhere you see it
application/jsonPlain JSONRFC 8259Every REST API
application/ld+jsonJSON-LD linked dataW3C JSON-LDStructured data in HTML, semantic web
application/problem+jsonProblem Details error bodyRFC 9457Standardized API error responses
application/vnd.api+jsonJSON:API documentsjsonapi.orgAPIs that follow the JSON:API spec
application/json-patch+jsonJSON Patch operationsRFC 6902HTTP PATCH request bodies
application/merge-patch+jsonJSON Merge PatchRFC 7386HTTP PATCH request bodies
application/geo+jsonGeoJSONRFC 7946Maps and geographic features
text/jsonNot registeredNoneLegacy 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.

FrameworkHelperHeader it sends
Expressres.json(data)application/json; charset=utf-8
Flaskreturn jsonify(data)application/json
Djangoreturn JsonResponse(data)application/json
Spring Boot@RestController returning an objectapplication/json
Next.js Route HandlerNextResponse.json(data)application/json
FastAPIreturn data from a path functionapplication/json
Laravelreturn response()->json($data) (see Laravel JSON API responses)application/json
Plain PHPheader('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:

  1. fetch with a string body and no header. The Fetch standard sets text/plain;charset=UTF-8 when 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.

  2. 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.

  3. 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.

  4. 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 http module, headers sent after res.write() are ignored. Call res.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.

Frequently Asked Questions

What is the correct Content-Type for JSON?
application/json. It is the media type registered for JSON in RFC 8259 and is what every modern framework and browser expects. Variants such as application/ld+json or application/problem+json are also valid because they use the registered +json structured suffix.
Is text/json a valid Content-Type?
No. text/json was never registered with IANA and does not appear in any JSON specification. Some older servers and client libraries still emit or accept it, but strict parsers such as Express's body parser will ignore a body labelled text/json unless explicitly configured to accept it.
Should I add charset=utf-8 to application/json?
You can, but it does nothing. RFC 8259 requires JSON exchanged between systems to be UTF-8 and defines no charset parameter for application/json, so compliant receivers ignore it. Many frameworks add it anyway for the benefit of older clients, and that is harmless.
Why does my API return 415 Unsupported Media Type?
The server received a request body with a Content-Type it does not accept, most often text/plain, application/x-www-form-urlencoded, or no header at all. Set Content-Type: application/json on the request and make sure the body is a JSON string, not a raw object.
What is the difference between Content-Type and Accept?
Content-Type describes the body you are sending. Accept tells the server which media types you are willing to receive back. A JSON client typically sends both: Content-Type: application/json on the request body and Accept: application/json to ask for a JSON response.
Does the Content-Type header make a browser download a JSON file?
No. Browsers display application/json inline as text, and Firefox and Chrome both offer a collapsible JSON viewer. A download is triggered by a Content-Disposition: attachment header or by a type the browser cannot render, such as application/octet-stream.

Check the Body, Not Just the Header

Paste any API response into the formatter to confirm it is valid JSON before you blame the Content-Type. Nothing leaves your browser.

Validate My JSON