DEV Community

Cover image for Understanding the Different POST Content Types
Aidas Bendoraitis
Aidas Bendoraitis

Posted on • Originally published at djangotricks.com

Understanding the Different POST Content Types

After more than 20 years of building for the web, this topic somehow kept slipping past me. It always felt obvious, so I never looked deeper. Recently I finally took the time to explore it properly, did some quick research, and now I’m sharing the results. Here’s a simple walkthrough of the different content types you can send in POST requests.

Standard Form Data

When you submit a basic HTML form like <form method="post" action=""></form>, for example a login form, the browser sends the data using the application/x-www-form-urlencoded content type. The body of the request looks like a URL-encoded query string, the same format typically used in GET requests.

Example: username=john_doe&password=pass123.

A POST request with this content type using the fetch API looks like this:

async function sendURLEncoded() {
    const params = new URLSearchParams();
    params.append('username', 'john_doe');
    params.append('email', 'john@example.com');
    params.append('password', 'Secret123');
    params.append('bio', 'This is a multi-line\nbio text.\nIt supports newlines!');

    const response = await fetch('/api/urlencoded/', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'X-CSRFToken': getCSRFToken()
        },
        body: params
    });
    const result = await response.json();
    console.log(result);
}
Enter fullscreen mode Exit fullscreen mode

On the Django side, validation with a Django form is the usual approach:

from django import forms
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from .forms import URLEncodedForm


class URLEncodedForm(forms.Form):
    username = forms.CharField(
        max_length=150,
        required=True,
        min_length=3,
    )
    email = forms.EmailField(
        required=True,
        error_messages={
            "required": "Email is required",
            "invalid": "Please enter a valid email address",
        },
    )
    password = forms.CharField(
        required=True,
        min_length=8,
    )
    bio = forms.CharField(
        required=False,
        max_length=500,
        widget=forms.Textarea,
        error_messages={"max_length": "Bio cannot exceed 500 characters"},
    )


@require_http_methods(["POST"])
def handle_urlencoded(request):
    """Handle application/x-www-form-urlencoded requests"""
    form = URLEncodedForm(request.POST)

    if not form.is_valid():
        return JsonResponse(
            {
                "status": "error",
                "errors": form.errors,
                "content_type": request.content_type,
            },
            status=400,
        )

    return JsonResponse(
        {
            "status": "success",
            "form_data": form.cleaned_data,
            "content_type": request.content_type,
        }
    )
Enter fullscreen mode Exit fullscreen mode

Form Data with Files

If your form includes file uploads, the browser switches to multipart/form-data. You enable it in HTML like this:

<form method="post" action="" enctype="multipart/form-data">
Enter fullscreen mode Exit fullscreen mode

A fetch-based upload looks like:

async function sendMultipart() {
    const formData = new FormData();
    formData.append('username', 'john_doe');
    formData.append('email', 'john@example.com');

    const fileInput = document.getElementById('file-input');
    if (fileInput.files[0]) {
        formData.append('avatar', fileInput.files[0]);
    }

    const response = await fetch('/api/multipart/', {
        method: 'POST',
        headers: {
            'X-CSRFToken': getCSRFToken()
        },
        body: formData
    });
    const result = await response.json();
    console.log(result);
}
Enter fullscreen mode Exit fullscreen mode

The Django view works the same way, just with request.FILES added.

from django import forms
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods


class MultipartForm(forms.Form):
    username = forms.CharField(
        max_length=150,
        required=True,
        min_length=3,
    )
    email = forms.EmailField(
        required=True,
    )
    avatar = forms.FileField(
        required=False,
    )

    def clean_avatar(self):
        avatar = self.cleaned_data.get("avatar")
        if avatar:
            if avatar.size > 5 * 1024 * 1024:
                raise forms.ValidationError("File size cannot exceed 5MB")

            allowed_types = ["image/jpeg", "image/png", "image/gif", "image/webp"]
            if avatar.content_type not in allowed_types:
                raise forms.ValidationError(
                    f'Invalid file type. Allowed types: {", ".join(allowed_types)}'
                )
        return avatar



@require_http_methods(["POST"])
def handle_multipart(request):
    """Handle multipart/form-data requests (forms with files)"""
    form = MultipartForm(request.POST, request.FILES)

    if not form.is_valid():
        return JsonResponse(
            {
                "status": "error",
                "errors": form.errors,
                "content_type": request.content_type,
            },
            status=400,
        )

    form_data = {}

    for key, value in form.cleaned_data.items():
        if hasattr(value, "read"):
            form_data[key] = {
                "name": value.name,
                "size": value.size,
                "content_type": value.content_type,
            }
        else:
            form_data[key] = value

    return JsonResponse(
        {
            "status": "success",
            "form_data": form_data,
            "content_type": request.content_type,
        }
    )        
Enter fullscreen mode Exit fullscreen mode

JSON String

A very common modern content type is application/json. Single-Page Applications and most JavaScript-heavy frontends rely on it.

Frontend example:

async function sendJSON() {
    const data = { 
        name: "John Doe", 
        email: "john@example.com", 
        age: 30
    };
    const response = await fetch('/api/json/', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRFToken': getCSRFToken()
        },
        body: JSON.stringify(data)
    });
    const result = await response.json();
    console.log(result)
}
Enter fullscreen mode Exit fullscreen mode

For validation, you can use Pydantic, which is a nice alternative to Django-REST-Framework serializers.

from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from pydantic import BaseModel, EmailStr, Field, ConfigDict, ValidationError


class JSONDataSchema(BaseModel):
    model_config = ConfigDict(
        json_schema_extra={
            "example": {
                "name": "John Doe", 
                "email": "john@example.com", 
                "age": 30,
            }
        }
    )

    name: str = Field(min_length=2, max_length=100, description="User's full name")
    email: EmailStr = Field(description="Valid email address")
    age: int = Field(ge=0, le=150, description="User's age")


@require_http_methods(["POST"])
def handle_json(request):
    """Handle application/json requests"""
    try:
        data = json.loads(request.body)
        validated = JSONDataSchema(**data)

        return JsonResponse(
            {
                "status": "success",
                "received": validated.model_dump(),
                "content_type": request.content_type,
            }
        )
    except json.JSONDecodeError:
        return JsonResponse(
            {
                "status": "error",
                "error": "Invalid JSON",
                "content_type": request.content_type,
            },
            status=400,
        )
    except ValidationError as e:
        return JsonResponse(
            {
                "status": "error",
                "errors": e.errors(),
                "content_type": request.content_type,
            },
            status=400,
        )
Enter fullscreen mode Exit fullscreen mode

Newline-Delimited JSON

application/x-ndjson (or simply NDJSON) is an experimental but handy format where each line is a separate JSON object. It’s useful for bulk imports – logs, analytics, and other large datasets.

async function sendNDJSON() {
    const ndjsonData = (
`{"name":"John","age":30}
{"name":"Jane","age":25}
{"name":"Bob","age":35}`
);
    const response = await fetch('/api/ndjson/', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-ndjson',
            'X-CSRFToken': getCSRFToken()
        },
        body: ndjsonData
    });
    const result = await response.json();
    console.log(result);
}
Enter fullscreen mode Exit fullscreen mode

The processing logic is standard: split, parse, validate.

import json
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods


@require_http_methods(["POST"])
def handle_ndjson(request):
    """Handle application/x-ndjson requests"""
    try:
        ndjson_content = request.body.decode("utf-8")
        lines = [
            json.loads(line) for line in ndjson_content.strip().split("\n") if line
        ]

        return JsonResponse(
            {
                "status": "success",
                "lines_count": len(lines),
                "data": lines,
                "content_type": request.content_type,
            }
        )
    except json.JSONDecodeError:
        return JsonResponse({"error": "Invalid NDJSON"}, status=400)
Enter fullscreen mode Exit fullscreen mode

Plain Text

Some systems accept plain text via text/plain, especially object storage services or endpoints meant for raw logs or unstructured content.

async function sendTextPlain() {
    const textData = `This is plain text content.
It can span multiple lines.
Line 3 of the content.`;

    const response = await fetch('/api/text/', {
        method: 'POST',
        headers: {
            'Content-Type': 'text/plain',
            'X-CSRFToken': getCSRFToken()
        },
        body: textData
    });
    const result = await response.json();
    console.log(result);
}
Enter fullscreen mode Exit fullscreen mode

The Django view is minimal and functional:

from django.http import JsonResponse
from django.views.decorators.http import require_http_methods


@require_http_methods(["POST"])
def handle_text_plain(request):
    """Handle text/plain requests"""
    text_content = request.body.decode("utf-8")
    return JsonResponse(
        {
            "status": "success",
            "received": text_content,
            "length": len(text_content),
            "content_type": request.content_type,
        }
    )
Enter fullscreen mode Exit fullscreen mode

HTML Text

You can also POST HTML content using text/html, which can be useful for wiki-style pages or HTML editors that save full documents.

async function sendHTML() {
    const htmlData = `<!DOCTYPE html>
<html>
<head><title>Example</title></head>
<body><h1>Hello World!</h1></body>
</html>`;
    const response = await fetch('/api/html/', {
        method: 'POST',
        headers: {
            'Content-Type': 'text/html',
            'X-CSRFToken': getCSRFToken()
        },
        body: htmlData
    });
    const result = await response.json();
    console.log(result);
}
Enter fullscreen mode Exit fullscreen mode

The Django view would accept the data like this (add your own validation):

from django.http import JsonResponse
from django.views.decorators.http import require_http_methods


@require_http_methods(["POST"])
def handle_html(request):
    """Handle text/html requests"""
    html_content = request.body.decode("utf-8")
    return JsonResponse(
        {
            "status": "success",
            "received": html_content,
            "length": len(html_content),
            "content_type": request.content_type,
        }
    )
Enter fullscreen mode Exit fullscreen mode

XML Data

Older or legacy integrations (especially SOAP-based ones) still rely on application/xml.

Here's a JavaScript example:

async function sendXML() {
    const xmlData = `<?xml version="1.0" encoding="UTF-8"?>
<user>
<name>John Doe</name>
<email>john@example.com</email>
<age>30</age>
</user>`;

    const response = await fetch('/api/xml/', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/xml',
            'X-CSRFToken': getCSRFToken()
        },
        body: xmlData
    });
    const result = await response.json();
    console.log(result);
}
Enter fullscreen mode Exit fullscreen mode

And the Django view can parse the XML data into a dictionary like this:

import xml.etree.ElementTree as ET
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods


@require_http_methods(["POST"])
def handle_xml(request):
    """Handle application/xml requests"""
    try:
        xml_content = request.body.decode("utf-8")
        root = ET.fromstring(xml_content)

        data = {
            child.tag: child.text 
            for child in root
        }

        return JsonResponse(
            {
                "status": "success",
                "root_tag": root.tag,
                "data": data,
                "content_type": request.content_type,
            }
        )
    except ET.ParseError:
        return JsonResponse({"error": "Invalid XML"}, status=400)
Enter fullscreen mode Exit fullscreen mode

SVG Image

SVG graphics can be sent as image/svg+xml, since they are XML-based.

async function sendSVG() {
    const svgData = `<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" />
</svg>`;
    const response = await fetch('/api/svg/', {
        method: 'POST',
        headers: {
            'Content-Type': 'image/svg+xml',
            'X-CSRFToken': getCSRFToken()
        },
        body: svgData
    });
    const result = await response.json();
    console.log(result);
}
Enter fullscreen mode Exit fullscreen mode

The Django view (add your own validation):

from django.http import JsonResponse
from django.views.decorators.http import require_http_methods


@require_http_methods(["POST"])
def handle_svg(request):
    """Handle image/svg+xml requests"""
    svg_content = request.body.decode("utf-8")
    return JsonResponse(
        {
            "status": "success",
            "received": svg_content,
            "length": len(svg_content),
            "content_type": request.content_type,
        }
    )
Enter fullscreen mode Exit fullscreen mode

Binary Data

For raw binary streams – images, audio, video, PDF, or any unknown byte sequence – the fallback is application/octet-stream.

Here is a JavaScript example of how to post it:

async function sendBinary() {
    const binaryData = new Uint8Array([72, 101, 108, 108, 111]);
    const response = await fetch('/api/binary/', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/octet-stream',
            'X-CSRFToken': getCSRFToken()
        },
        body: binaryData
    });
    const result = await response.json();
    console.log(result);
}
Enter fullscreen mode Exit fullscreen mode

The Django view would be the most straightforward, because request.body is already coming as bytestring:

from django.http import JsonResponse
from django.views.decorators.http import require_http_methods


@require_http_methods(["POST"])
def handle_binary(request):
    """Handle application/octet-stream requests (raw binary data)"""
    binary_data = request.body
    return JsonResponse(
        {
            "status": "success",
            "size": len(binary_data),
            "content_type": request.content_type,
            "first_bytes": list(binary_data[:10]),
        }
    )
Enter fullscreen mode Exit fullscreen mode

Protocol Buffers

Protocol Buffers use application/x-protobuf, and compared to JSON they’re usually around 50% smaller and faster to parse. I won't cover the code in this article, but do your research if you need speed.

Code to Play Around with

The rough distribution of content-type usage in real-world APIs is this:

  • JSON: ~85–90%
  • Multipart: ~10–15%
  • Everything else: <5%

If you want to experiment with all the content types, and see the code in context, here's a repo as a great playground:
https://github.com/archatas/django-post-content-types

Top comments (0)