CodeGym /Courses /Spring REST & MVC /Data Formats in HTTP

Data Formats in HTTP

Spring REST & MVC
Level 2, Lesson 3
Available

1. Data format as part of the HTTP contract

When you first see HTTP, it seems that the main thing is the URL and JSON in the body. But there is one unpleasant truth: body is just a set of bytes, that is, a “raw data stream.” Without a hint about how exactly to interpret those bytes, client and server start guessing — and guessing in IT usually ends with very creative bugs.

Imagine someone brought you a flash drive and said, “There is a document on it.” Which one exactly? Word? PDF? An image? A text file? With body in HTTP, it is the same story: if the server does not know the input format, it will not be able to parse the data correctly. And if the client does not know the response format, it will not be able to read the result reliably. That is why HTTP has the concept of media type (data type, or message format) and two key headers that most often appear in REST APIs: Content-Type and Accept.

Here it is important not to mix up the roles. Content-Type is “what I actually sent in this body.” Accept is “what I want to get in response.” They look similar in form, but their meaning is different. It is precisely this difference — and the habit of mixing them up — that underlies half the funny stories about “why my server does not understand my JSON, it looks like JSON.”

To make this meaning tangible, let us think of Task Tracker API as a service that communicates with external clients. The client reads and writes tasks, and it is important for it to understand: “I am sending JSON” and “I want to receive JSON.” That is not assumed “by default” — it is part of the contract.

We have already broken the HTTP message into start line, headers, and body. Now it is time to dispel another illusion: body by itself explains nothing. Until the sides agree on a format through headers, it is just a set of bytes.

2. Media type: the name of the format

The term media type (it is often also called MIME type, out of old habit) is the standardized name of a data format. It looks like type/subtype. For example, application/json, text/plain, image/png. Important: this is not about Java types (String, Task, List<Task>), but about the format of data representation in HTTP.

The meaning is simple: if you see application/json, you understand that the body is JSON. If it is text/plain, then it is plain text, and nobody promised there would be structured fields. If it is application/pdf, then it is a PDF, and you definitely should not try to deserialize it as DTO, even if you really want to. A media type is like a label on a container: it does not make the contents tastier, but it helps you avoid drinking shampoo thinking it is lemonade.

Here is a small useful table right away; it will come up throughout the course, even if today we stay in a JSON-first context:

Media type What it means Where it appears in Task Tracker API
application/json JSON data the main format for successful responses and requests
application/problem+json a JSON error response in a standardized form error responses (errors as part of the contract)
text/plain plain text sometimes in “test” endpoints or in simple services; in our API, this is more of an anti-example
multipart/form-data a composite request made up of multiple parts a typical format for file uploads (mentioned as an example)
application/octet-stream “just binary data” often used as a fallback for file downloads (mentioned as an example)

We are not going to expand the error model here; right now we only need one fact: even an error arrives not as “some JSON,” but in an explicitly declared format.

Pay attention to application/problem+json. It has the suffix +json. That is not “magic,” but a clue: the data is still JSON, but the meaning of the structure is special (errors). If you see +json, you can expect JSON parsing, but with different semantics.

Another nuance: a media type may have parameters, for example charset. It looks like this:

Content-Type: application/json; charset=utf-8

We will not make a separate religion out of this, but it is useful to remember: parameters are part of the format declaration.

3. Content-Type: the format of the sent body

If you imagine an HTTP request as a package, then Content-Type is the “what is inside” sticker, attached by the sender. It describes the format of the message body that you are sending. A request and a response can both have Content-Type, because both a request and a response can have a body.

Usually the most critical place for Content-Type is requests with a body: POST, PUT, PATCH. If the client sends JSON but does not say Content-Type: application/json, the server has every right not to try guessing and to say: “Sorry, I do not understand what you brought me.” In HTTP this is often expressed with status 415 Unsupported Media Type (or another error depending on the server), but the main point is that the problem happens before business logic. The server may not even get to the idea of “create a task,” because it gets stuck at the stage of “I cannot read the body.”

A mini-example of a “correct” create request for a task, plain HTTP that is easy to imagine both in Postman and in a .http file:

# The client explicitly tells the server: the request body is JSON (this is the format contract)
POST /api/v1/tasks HTTP/1.1
Content-Type: application/json

{"title":"Write docs","description":"HTTP lesson"}

Here Content-Type answers the question: “How should I interpret this piece of text after the blank line?” And that is especially important because visually JSON and “just text” can look the same (the symbols are the same), but the processing meaning is different.

Another example is a response. The server must also honestly say in what format it returned the data:

# The server confirms: the response is JSON too (the client does not need to guess from the contents)
HTTP/1.1 201 Created
Content-Type: application/json

{"id":"42","title":"Write docs"}

If the client receives a body without Content-Type, it is again forced to guess. Sometimes clients guess “out of habit,” and then one day the server returns HTML (for example, an error page), and client-side deserialization turns into a sad amusement ride.

A small analogy to reinforce the idea. Content-Type is not “what I want to send,” but “what I actually sent.” If you wrote a letter in Russian but put “English inside” on the envelope, the recipient would read the first two lines and decide you are mocking them. The server reacts in much the same way, only without passive aggression — it simply returns an error.

And finally: if the request has no body (a typical GET), then Content-Type in the request is often not needed. But “often” does not mean “never”: there are clients that set it by template. That is not a crime, but it is not a super useful habit either. The main thing is not to mix this up: Content-Type only makes sense where there is a body that needs a format.

4. Accept: the format of the expected response

If Content-Type is the label on what you are sending, then Accept is the list of what you are ready or want to receive in response. It is sent by the client, and in meaning it is part of the “negotiation”: the client tells the server which response formats are acceptable.

The simplest and most common option in our JSON-first world:

# The client fixes the expectation: "I want JSON in response"
GET /api/v1/tasks HTTP/1.1
Accept: application/json

This literally reads as: “Server, please give me the response in JSON.” The server can do that and return:

# The server in the response no longer uses Accept — it reports the actual format through Content-Type
HTTP/1.1 200 OK
Content-Type: application/json

[{"id":"42","title":"Write docs"}]

Notice the beauty — and the strictness — of the protocol: in the request, the client writes Accept, and in the response, the server writes Content-Type. These are two sides of the same deal.

In reality, Accept can be more complex: you can list several formats separated by commas, use */* (ready to accept anything), and sometimes you will see “weights” (q=...) where the client expresses preferences. But at the Junior level, it is useful to hold on to the basic model: Accept is the expected response format.

Why is it still important to understand Accept in a JSON-first API, even if “we always return JSON”? Because:

1) Clients are different. A browser, for example, is often ready to accept HTML by default. If the server suddenly returns an HTML error page, a client expecting JSON will suffer.

2) In a normal API, errors also have a format. And often it is not the same format as a “successful response”. Even if it is still JSON, the media type may differ.

3) Accept makes the contract explicit. Explicitness is when you do not rely on telepathy between yourself and the server. Telepathy in IT is basically “undefined behavior,” only without a warning.

To avoid confusing Content-Type and Accept once and for all, it is useful to look at them side by side:

Header Who sends it What it is about Which question it answers
Content-Type the sender of the current message (client in a request, server in a response) the actual format of the current body “In what format is this body?”
Accept client the desired format of the response “In what format do I want to receive the response?”

In short: Content-Type is about the present (what has already been sent), Accept is about the expectation (what we want to receive).

5. Content negotiation: choosing a format

In an ideal world, the dialog looks like this: the client asks for one of the supported formats in Accept, the server chooses an appropriate one, and in the response sets Content-Type. This is the simplified version of content negotiation — format negotiation.

In practice, in most application REST APIs, the server supports a limited set of formats. Often this is application/json for successful responses and a few special cases. Then content negotiation boils down to a very simple rule: “If the client wants JSON — we return JSON. If it wants something strange — we do not return it.”

If the server cannot return any of the formats the client asked for, it may return 406 Not Acceptable. We will not turn this into mandatory knowledge to memorize right now, but it is important to understand the meaning: the problem is not the data, but that the sides did not agree on the response format.

Another clear diagram to help lock this into your head:

flowchart LR
    C[Client] -->|"Request: Accept"| S[Server]
    S -->|"Response: Content-Type"| C

There is an important symmetry here: the request says “what I want,” the response says “what I gave.”

In the context of Task Tracker API, it is useful to keep a discipline in mind right away: a client that wants predictability sends Accept: application/json. And a server that respects the client always responds with Content-Type: application/json (or another explicitly stated format), so that the client does not guess from the contents.

And yes, you can say: “But JSON is visible anyway, there are curly braces.” You can. And then one day you get an HTML page in the body and realize that curly braces are not a universal marker of happiness.

6. Examples for Task Tracker API

When you start learning, it is very useful to have “manual” requests that you can run and observe not only the JSON, but also the headers. That is why a good habit is a .http file (for example, in IntelliJ IDEA). Even if your API is not fully implemented yet, the structure of the request already teaches you to think in terms of a contract.

Below is a small snippet that you can put into requests/day2-media-types.http and use as a “thinking checklist”:

### List tasks (we expect JSON)
# Accept fixes the expected response format
GET http://localhost:8080/api/v1/tasks
Accept: application/json

The point here is not for it to “work” right now (although if you already have stubs, great), but to train yourself: even for GET requests, it is useful to explicitly fix the expected response format.

Now a create request where Content-Type is critical:

### Create task (sending JSON)
# Content-Type describes the format of the request body (input data)
POST http://localhost:8080/api/v1/tasks
Content-Type: application/json
# Accept describes the format we want to get in response
Accept: application/json

{"title":"Write docs","description":"HTTP lecture"}

Here we made two important statements. We honestly told the server how to read the input. And we honestly told it which response we want to receive.

The third example is what people usually forget: errors also have a format. Even if we do not discuss the error model in detail, we can still see the Content-Type header in an error response. For example, if the client sent a body “in the wrong format,” the server may respond with something like:

# An error also has a format: this is not "ordinary successful JSON", but a separate media type for problems
HTTP/1.1 415 Unsupported Media Type
Content-Type: application/problem+json

{"title":"Unsupported Media Type","status":415}

Do not let it confuse you that the body here is JSON. We do not need the whole error-handling layer right now. What matters is to see the mechanism: Content-Type honestly says that this is a special error format, not an “ordinary successful response.” The client can handle it differently, without trying to deserialize it into a “task DTO.”

And here is a useful anti-example that explains why “I sent JSON, but did not specify Content-Type” is a real problem:

### Anti-example: JSON body, but Content-Type is text/plain
# Visually this is JSON, but by contract you declared plain text
POST http://localhost:8080/api/v1/tasks
Content-Type: text/plain

{"title":"This looks like JSON, but server may disagree"}

The human eye says: “Well, that is JSON.” The server looks at the header and replies: “You said this is plain text. I believed you. I believed you for nothing, but according to the protocol, it is correct.” And then different behaviors are possible, but none of them will be a stable contract.

To bring this down to programming, you can think of headers as a regular map structure. Yes, in real HTTP there are nuances (repeated headers, letter case does not matter, and so on), but the basic idea for a beginner is this:

import java.util.Map;

public class HeadersDemo {
    public static void main(String[] args) {
        // Headers are a separate layer of the contract: not "Postman magic," but ordinary key-value pairs.
        Map<String, String> headers = Map.of(
                // Content-Type: what format the current body has (when there is a body)
                "Content-Type", "application/json",
                // Accept: what response format we expect from the server
                "Accept", "application/json"
        );

        // Get the "expectation" by response format
        System.out.println(headers.get("Accept")); // application/json
    }
}

It is basic, but useful: headers are a separate layer of the contract, not “some Postman magic.”

7. Common nuances: charset and an empty body

When you start working with media types, small details appear that sound like nitpicking, but in practice save hours of debugging. The most common one is encoding. In HTTP it is usually specified by the charset parameter. For JSON, the standard and most expected option is UTF-8. Sometimes you will see:

Content-Type: application/json; charset=utf-8

That is normal. Sometimes you will not see charset at all. That is also often normal, because many clients and servers consider UTF-8 a reasonable default. The main thing is not to try to force JSON into a non-standard encoding and hope that everyone around you is reading your mind.

The second detail is an empty body. A request can have no body, and a response can have no body. We have already seen in status codes that 204 No Content means “successful, but with no body.” Here it is important not to require Content-Type where there is no body. If there is no body, saying “what format is the body” simply makes no sense.

The third detail is the difference between “the client did not specify Accept” and “the client specified Accept: */*.” Often clients set */*, which means “I do not care.” That is convenient for a browser, but for an API client that wants strictness, it is sometimes a bad habit. If you want to predictably receive JSON, it is better to say it directly: Accept: application/json.

And the last nuance is psychological. A beginner often thinks: “Well, I sent JSON, the server must figure it out.” The server is not obliged to. And even if it does figure it out, that will be not a contract, but just “luck.” In a proper API, behavior should be reproducible: the same request should be interpreted the same way in different environments, with different clients, and in a year, when you have already forgotten how it “actually worked.”

8. Typical mistakes when working with Content-Type and Accept

Mistake #1: mixing up Content-Type and Accept.
This is a classic: the client sends a POST with JSON, but sets Accept: application/json and forgets Content-Type. The result is logical: the client said it wants JSON in response, but said nothing about how the server should read the input. The server is not obliged to guess and often returns an error before it even tries to process the request.

Mistake #2: sending JSON with Content-Type: text/plain “because it is text.”
Yes, JSON is text by nature. But text/plain means “unstructured text.” For many servers and libraries, that is a different processing path. As a result, the server either reads the body as a string and does not try to parse it as JSON, or it tries, but you get instability. If you are sending JSON, call things by their names: application/json.

Mistake #3: not looking at Content-Type in the response and determining the format “by eye.”
As long as the response is successful and returns JSON, you can fool yourself. But on errors, the server may return a different format (including an HTML page, text, or a special JSON error format). If the client just “deserializes everything as a task DTO,” it breaks exactly when it should behave carefully. The habit is simple: always look at the first line of the response (the status) and at Content-Type, and only then at the body.

Mistake #4: thinking Accept is unnecessary because “the server always returns JSON.”
Even if the server really does always return JSON, Accept is still good discipline. It fixes the client’s expectation and makes the request self-documenting. This is especially important in a team: when you open a .http file six months later, you immediately understand what format is expected. It is like a comment, only built into the protocol.

Mistake #5: forgetting that a request without a body does not have to have Content-Type.
Sometimes people put Content-Type: application/json into every request by template, including GET. That is not always harmful, but it often confuses things: the header looks like there is a body and it needs to be read as JSON. When you start debugging complex scenarios, such “noisy” headers only get in the way. Good style: Content-Type where there is actually a body.

1
Task
Spring REST & MVC, level 2, lesson 3
Locked
Extraction of `Content-Type` and `Accept`
Extraction of `Content-Type` and `Accept`
1
Task
Spring REST & MVC, level 2, lesson 3
Locked
Is the response compatible with the client's expectation
Is the response compatible with the client's expectation
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION