Parsing JSON is conceptually simple — text in, structured data out — but the idioms differ sharply across languages. Go uses struct tags and reflection. Rust uses serde with derive macros. Python uses dicts or, increasingly, dataclasses and pydantic. Here's a side-by-side comparison with the same example in each.
The example payload
Throughout this article we'll parse this JSON:
{
"id": 42,
"name": "Alice",
"email": "alice@example.com",
"active": true,
"tags": ["admin", "verified"],
"created_at": "2026-01-15T10:30:00Z"
}
Go: struct tags and encoding/json
Go's standard library handles JSON via encoding/json. You declare a struct, annotate fields with tags, and call json.Unmarshal:
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Active bool `json:"active"`
Tags []string `json:"tags"`
CreatedAt time.Time `json:"created_at"`
}
var user User
err := json.Unmarshal(data, &user)
if err != nil {
log.Fatal(err)
}
The struct tags map JSON field names to Go field names. Without them, Go matches case-insensitively but won't handle snake_case → CamelCase automatically.
Strengths: Standard library, zero dependencies, predictable performance. The compiled binary doesn't need a JSON library at runtime.
Weaknesses: Reflection-based; slower than the fastest third-party parsers. Error messages are vague ("cannot unmarshal X into field Y"). No native sum types means handling polymorphic JSON requires custom UnmarshalJSON methods or interface{} maps. For high-throughput services, consider jsoniter or json-iterator/go.
Rust: serde and derive macros
Rust's de-facto JSON library is serde_json, built on the serde serialization framework. The code looks deceptively similar to Go:
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
struct User {
id: u32,
name: String,
email: String,
active: bool,
tags: Vec<String>,
created_at: chrono::DateTime<chrono::Utc>,
}
let user: User = serde_json::from_str(data)?;
Two differences from Go matter. First, the #[derive] macros generate the parsing code at compile time, not at runtime via reflection. That's why serde is among the fastest JSON parsers in any language. Second, Rust's type system means missing or null required fields are caught at parse time, not later when you try to use them.
Strengths: Speed (often 2-5× faster than Go's standard library). Compile-time validation. Excellent error messages with line/column for malformed JSON.
Weaknesses: Borrow checker can complicate ergonomics for streaming or zero-copy parsing. Adding the serde ecosystem to a project requires several crates (serde, serde_json, often chrono for dates).
Python: dicts, dataclasses, and pydantic
Python has three common approaches, in order of safety:
Plain dicts with json.loads():
import json
user = json.loads(data)
print(user["name"]) # "Alice"
Simple, but no type checking. Typos in field names raise KeyError at runtime.
Dataclasses with manual conversion:
from dataclasses import dataclass
from datetime import datetime
@dataclass
class User:
id: int
name: str
email: str
active: bool
tags: list[str]
created_at: datetime
raw = json.loads(data)
user = User(**raw, created_at=datetime.fromisoformat(raw["created_at"].replace("Z", "+00:00")))
Dataclasses give you structure and type hints, but they don't validate or convert types from JSON. You write the glue.
Pydantic is what most modern Python codebases reach for:
from pydantic import BaseModel
from datetime import datetime
class User(BaseModel):
id: int
name: str
email: str
active: bool
tags: list[str]
created_at: datetime
user = User.model_validate_json(data)
Pydantic handles type coercion, validation, default values, and error reporting. The model_validate_json method parses straight from the JSON string — no intermediate json.loads needed.
Strengths of pydantic: Friendly errors with full paths. Built-in validators for common types (email, URLs). Plays well with FastAPI, where it's the standard for request/response models.
Weaknesses: Slower than serde or even Go's standard library. Heavier dependency. Pydantic v2 is much faster than v1 (rewritten in Rust) but still slower than Rust-native parsers.
Performance comparison
For a 1MB JSON document on a modern CPU, rough numbers:
- Rust (serde_json): ~50-100MB/sec parsing, near-zero allocation overhead with borrowed strings.
- Go (encoding/json): ~30-50MB/sec. Third-party parsers like
jsoniterhit 60-80MB/sec. - Python (orjson): ~50-80MB/sec — orjson is C-extension-based and rivals serde.
- Python (stdlib json): ~10-20MB/sec.
- Python (pydantic v2): Adds ~30-50% overhead over raw parsing for validation.
For most applications, none of this matters — your JSON is small and parsing is not the bottleneck. For high-throughput services, choose the language first and the JSON library second.
Error handling philosophy
Each language reflects its broader philosophy in JSON parsing. Go returns an error and expects you to check it. Rust returns a Result<T, Error> and won't let you ignore the error case. Python raises an exception — usually json.JSONDecodeError or, with pydantic, ValidationError.
Pydantic's error format is the most informative out of the box: it returns a list of dicts, each with the path to the bad field, the error type, and the input that failed. Go's errors give you a position but rarely a path. Rust's errors give you both.
Choosing for your project
If you're building a public API in Go, the standard library is fine — don't reach for a third-party parser until you've profiled and found JSON to be a bottleneck. If you're writing Rust, use serde from day one; it's the ecosystem default. If you're writing Python and care about correctness, use pydantic; if you only care about speed and the input is trusted, use orjson with plain dicts.
For browser-side parsing during development and debugging, the JSON formatter remains the fastest way to inspect a document by eye. For programmatic checks across many files, see our Python formatting guide and JavaScript formatting.