Skip to content

Enviar JSON por POST e ler a resposta JSON snippet

A tarefa com que todo o cliente de API começa.

A tarefa com que todo o cliente de API começa. A armadilha é a posse da serialização: alguns clientes serializam por você (httpx json=, reqwest .json(), PostAsJsonAsync), a maioria não — você tem de codificar o corpo como JSON E definir Content-Type: application/json, ou o servidor descodifica o seu payload como formulário em silêncio. A segunda armadilha é o caminho do erro: uma resposta 4xx também transporta um corpo que vale a pena ler. C, C++ e Zig ficam de fora: não existe um par HTTP+JSON na stdlib.

Receita executável · 11 linguagens
HTTP & APIshttppostjsonapiserialize

Every language

11 linguagens, 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 →