Skip to content

Envoyer du JSON en POST et lire la réponse JSON snippet

La tâche par laquelle tout client d'API commence.

La tâche par laquelle tout client d'API commence. Le piège est la propriété de la sérialisation : certains clients sérialisent pour toi (httpx json=, reqwest .json(), PostAsJsonAsync), la plupart non — tu dois encoder le corps en JSON toi-même ET définir Content-Type: application/json, sinon le serveur décode silencieusement ta charge utile comme un formulaire. Le deuxième piège est le chemin d'erreur : une réponse 4xx transporte quand même un corps qui mérite d'être lu. C, C++ et Zig sont omis : aucun couple HTTP+JSON n'existe en stdlib.

Recette exécutable · 11 langages
HTTP & APIshttppostjsonapiserialize

Every language

11 langages, copy-ready. One at a time with syntax highlighting, or all inline.

JSJavaScript
const res = await fetch('https://api.example.com/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Ada', role: 'admin' }),
});

if (!res.ok) {
  // 4xx/5xx bodies carry the reason — read them
  throw new Error(`HTTP ${res.status}: ${await res.text()}`);
}
const created = await res.json();
console.log(created.id);

fetch never serializes objects — JSON.stringify is on you, always. Setting the header is on you too.

TSTypeScript
interface User {
  id: string;
  name: string;
}

const res = await fetch('https://api.example.com/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Ada', role: 'admin' }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);

const created = (await res.json()) as User; // trust, not a check
console.log(created.name);

Same as JavaScript plus a cast for the response. For untrusted APIs, decode through a schema library instead of as.

GoGo
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
)

func main() {
	payload, _ := json.Marshal(map[string]string{"name": "Ada", "role": "admin"})

	res, err := http.Post("https://api.example.com/users",
		"application/json", bytes.NewReader(payload))
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	var created struct {
		ID   string `json:"id"`
		Name string `json:"name"`
	}
	if res.StatusCode >= 400 {
		panic(fmt.Sprintf("HTTP %d", res.StatusCode))
	}
	if err := json.NewDecoder(res.Body).Decode(&created); err != nil {
		panic(err)
	}
	fmt.Println(created.ID)
}

http.Post is the one-liner; the anonymous struct decodes a one-off response shape without a named type. Production: add a context (see the timeout recipe).

RsRust
use serde::Serialize;
use serde_json::Value;

#[derive(Serialize)]
struct NewUser<'a> {
    name: &'a str,
    role: &'a str,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let created: Value = reqwest::Client::new()
        .post("https://api.example.com/users")
        .json(&NewUser { name: "Ada", role: "admin" }) // sets the header too
        .send()
        .await?
        .error_for_status()?
        .json()
        .await?;

    println!("{}", created["id"]);
    Ok(())
}

.json() serializes AND sets Content-Type. Value is the sketch type — derive Deserialize on a real struct in production code.

PHPPHP
$ctx = stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' => "Content-Type: application/json\r\n",
        'content' => json_encode(['name' => 'Ada', 'role' => 'admin']),
        'timeout' => 5,
        'ignore_errors' => true, // make 4xx/5xx bodies readable
    ],
]);
$body = file_get_contents('https://api.example.com/users', false, $ctx);

$created = json_decode($body, true, flags: JSON_THROW_ON_ERROR);
echo $created['id'];

ignore_errors => true turns HTTP errors into readable bodies instead of a warning plus false. JSON_THROW_ON_ERROR — the default silent null is a classic bug.

PyPython
import httpx

response = httpx.post(
    'https://api.example.com/users',
    json={'name': 'Ada', 'role': 'admin'}, # serializes + sets the header
    timeout=5.0,
)
response.raise_for_status()
created = response.json()
print(created['id'])

json= and data= are different dials: json= sends application/json, data= form-encodes. Mixing them up is the #1 httpx/requests bug.

C#C#
using System.Net.Http.Json; // PostAsJsonAsync lives here

using var client = new HttpClient();

var response = await client.PostAsJsonAsync(
    "https://api.example.com/users",
    new { Name = "Ada", Role = "admin" });

response.EnsureSuccessStatusCode();
var created = await response.Content.ReadFromJsonAsync<User>();
Console.WriteLine(created?.Id);

record User(string Id, string Name);

System.Net.Http.Json is the rare stdlib that does the whole task in one call: serialize, header, POST, deserialize the response.

JvJava
HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("https://api.example.com/users"))
        .timeout(Duration.ofSeconds(5))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(
                "{\"name\":\"Ada\",\"role\":\"admin\"}"))
        .build();

HttpResponse<String> response = HttpClient.newHttpClient()
        .send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) {
    throw new IllegalStateException("HTTP " + response.statusCode());
}
System.out.println(response.body());

The JDK has no JSON parser — the hand-written string is the honest stdlib answer. Real code serializes and parses with Jackson or Gson.

SwSwift
import Foundation

struct NewUser: Codable { let name: String; let role: String }
struct User: Codable { let id: String; let name: String }

var request = URLRequest(url: URL(string: "https://api.example.com/users")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(NewUser(name: "Ada", role: "admin"))

let (data, response) = try await URLSession.shared.data(for: request)
guard let http = response as? HTTPURLResponse,
      (200..<300).contains(http.statusCode) else {
    throw URLError(.badServerResponse)
}
let created = try JSONDecoder().decode(User.self, from: data)
print(created.id)

Codable covers both directions. JSONEncoder/JSONDecoder are value types you can configure — keyDecodingStrategy .convertFromSnakeCase for snake_case APIs.

KtKotlin
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.time.Duration

@Serializable
data class NewUser(val name: String, val role: String)

@Serializable
data class User(val id: String, val name: String)

val payload = Json.encodeToString(NewUser("Ada", "admin"))

val request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/users"))
    .timeout(Duration.ofSeconds(5))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(payload))
    .build()

val response = HttpClient.newHttpClient()
    .send(request, HttpResponse.BodyHandlers.ofString())
check(response.statusCode() < 400) { "HTTP ${response.statusCode()}" }

val created = Json.decodeFromString<User>(response.body())
println(created.id)

kotlinx.serialization is the Kotlin-native (de)serializer — compile-time generated, no reflection. The HTTP layer is still java.net.http.

RbRuby
require 'net/http'
require 'json'
require 'uri'

uri = URI('https://api.example.com/users')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true

request = Net::HTTP::Post.new(uri.request_uri,
                              'Content-Type' => 'application/json')
request.body = { name: 'Ada', role: 'admin' }.to_json

response = http.request(request)
raise "HTTP #{response.code}" unless response.is_a?(Net::HTTPSuccess)

created = JSON.parse(response.body)
puts created['id']

Net::HTTP leaves both serialization and the header to you. Rails has to_json on tap; plain Ruby needs require 'json'.

Keep going

Read the http-status-codes cheatsheet →