PHP json_encode(): Deep Guide, Real-World Patterns, and Safe Defaults

The last time I had to debug a “mystery” API bug, the culprit wasn’t the database or the network. It was JSON. A client expected numbers, we sent strings; a UTF-8 edge case crept in; an error was swallowed and a silent failure broke a mobile app. If you work in PHP, json_encode() is one of those tools you call without thinking—until it bites. That’s why I treat it as a core API surface, not a trivial helper.

You should expect more from your JSON encoding than “it works.” You need predictable types, valid Unicode, errors that are actionable, and performance that scales from a tiny webhook to a bulk export job. In this post, I’ll show how I approach jsonencode() in modern PHP, what options matter in real systems, and how to build safe patterns you can reuse. You’ll see complete examples, common mistakes I see in reviews, and how to decide when jsonencode() is the right tool and when it isn’t.

Mental model: What json_encode() actually does

Think of json_encode() as a strict translator. It walks your PHP value and writes a JSON representation. It does not “clean” your data, and it won’t invent structure. If something is invalid for JSON, the translator either fails or makes a best effort based on your options.

Here’s the core signature:

string|false json_encode(mixed $value, int $flags = 0, int $depth = 512)

In practice, there are three outcomes you should design for:

  • A JSON string that matches the expected schema.
  • false when the value can’t be encoded.
  • An exception when you enable JSONTHROWON_ERROR (my default choice).

I like to treat encoding as a “boundary” action. You do it right before you send data out: to an API response, a queue, a cache, or a log. If you encode too early, you lose type information and make validation harder. If you encode too late, you risk leaking invalid or partially encoded data.

Parameters and flags you’ll actually use

I keep a short list of options that show up in production code. The rest are specialized and should be used with intent.

Value

The $value can be a scalar, array, or object. Objects are encoded by public properties unless they implement JsonSerializable.

Flags I use regularly

  • JSONTHROWON_ERROR: get exceptions instead of false. This is the single most important flag for robust systems.
  • JSONUNESCAPEDUNICODE: preserve readable Unicode, helpful for logs and APIs.
  • JSONUNESCAPEDSLASHES: avoid escaping / in URLs, more readable output.
  • JSONPRESERVEZERO_FRACTION: keep 1.0 as 1.0 instead of 1.
  • JSONNUMERICCHECK: useful for legacy string numbers, but dangerous if you rely on strings (I avoid it unless necessary).
  • JSONPARTIALOUTPUTONERROR: a compromise flag, but I rarely accept partial output for APIs.

Depth

Depth prevents runaway recursion. Default 512 is fine for most applications. If you encode a deeply nested structure (like a tree or graph), consider flattening or trimming before encoding. A low depth is a sign of a tight contract.

Safe defaults I use in real projects

I often centralize encoding into a helper to avoid inconsistencies. Here’s a pattern I use when I want safe JSON for APIs and logs:

<?php

function safejsonencode(mixed $value): string

{

// Combine the flags I want across the codebase.

$flags = JSONTHROWON_ERROR

| JSONUNESCAPEDUNICODE

| JSONUNESCAPEDSLASHES

| JSONPRESERVEZERO_FRACTION;

return json_encode($value, $flags, 512);

}

// Example usage

$payload = [

‘status‘ => ‘ok‘,

‘price‘ => 12.50,

‘currency‘ => ‘USD‘,

‘user‘ => [

‘name‘ => ‘Elena García‘,

‘profileUrl‘ => ‘https://example.com/u/elena‘,

],

];

echo safejsonencode($payload);

This isn’t magical, but it removes “surprise flags” across the system and standardizes error handling. If encoding fails, I want a clear exception that I can trace.

Arrays, objects, and how JSON shapes can surprise you

PHP arrays are hybrid structures; they can be lists or maps. JSON has separate types for arrays and objects. When you encode a PHP array, PHP decides which JSON type to use based on the keys.

If keys are sequential integers starting at 0, PHP outputs a JSON array. If not, it outputs a JSON object. This is a frequent source of “why is my client broken?” issues.

<?php

$asList = [

‘Mercury‘,

‘Venus‘,

‘Earth‘,

];

$asMap = [

1 => ‘Mercury‘,

2 => ‘Venus‘,

3 => ‘Earth‘,

];

echo json_encode($asList);

// ["Mercury","Venus","Earth"]

echo json_encode($asMap);

// {"1":"Mercury","2":"Venus","3":"Earth"}

If you need a JSON array but your keys aren’t sequential, reindex first:

<?php

$planets = [

1 => ‘Mercury‘,

2 => ‘Venus‘,

3 => ‘Earth‘,

];

$asArray = array_values($planets);

echo json_encode($asArray);

// ["Mercury","Venus","Earth"]

Objects and JsonSerializable

I usually prefer explicit JSON shapes for domain objects. JsonSerializable gives you a clean boundary and avoids leaking internal state.

<?php

class Invoice implements JsonSerializable

{

public function construct(

public string $id,

public float $total,

public string $currency,

public array $items,

) {}

public function jsonSerialize(): mixed

{

// Expose only what you want in JSON

return [

‘id‘ => $this->id,

‘total‘ => $this->total,

‘currency‘ => $this->currency,

‘items‘ => $this->items,

];

}

}

$invoice = new Invoice(

‘inv2026001‘,

149.95,

‘USD‘,

[

[‘name‘ => ‘USB-C Hub‘, ‘qty‘ => 1, ‘unitPrice‘ => 49.99],

[‘name‘ => ‘Keyboard‘, ‘qty‘ => 1, ‘unitPrice‘ => 99.96],

]

);

echo jsonencode($invoice, JSONTHROWONERROR);

I like this approach because it turns JSON encoding into a deliberate API, not an accidental side effect of public properties.

Handling errors the right way

If you don’t use JSONTHROWONERROR, you have to check for false and inspect jsonlast_error(). That’s fine, but I prefer exceptions because they prevent silent failures.

Here’s a robust pattern that works even if you can’t use exceptions:

<?php

function encodewitherror_check(mixed $value): string

{

$json = json_encode($value);

if ($json === false) {

$error = jsonlasterror_msg();

throw new RuntimeException("JSON encoding failed: {$error}");

}

return $json;

}

When you can use exceptions, it’s simpler:

<?php

function encodeorthrow(mixed $value): string

{

return jsonencode($value, JSONTHROWONERROR);

}

In production, I also log the data type and a summarized preview of the value when encoding fails. I avoid logging full payloads to prevent sensitive data leaks.

Unicode, invalid UTF-8, and why it matters

JSON requires valid UTF-8. If your string contains invalid bytes, encoding fails (or produces weird output if you ignore errors). This becomes a real problem when your data comes from external sources: CSV imports, scraped content, legacy databases, or files with mixed encodings.

I use two strategies:

  • Validate early: sanitize or normalize input before encoding.
  • Handle invalid bytes explicitly: either ignore or replace invalid UTF-8.

Here is a clear example of a sanitization pass:

<?php

function normalize_utf8(string $text): string

{

// Convert to UTF-8, replacing invalid sequences

return mbconvertencoding($text, ‘UTF-8‘, ‘UTF-8‘);

}

$raw = "Title with invalid byte: \xC3";

$clean = normalize_utf8($raw);

echo jsonencode([‘title‘ => $clean], JSONTHROWONERROR);

If you want the encoder to handle invalid bytes directly, you can use flags such as JSONINVALIDUTF8IGNORE or JSONINVALIDUTF8SUBSTITUTE. I avoid these for user-facing APIs unless I have a clear policy for data loss. For logs or telemetry, substitution is reasonable.

Precision, numeric strings, and money values

Numbers are the most common source of subtle bugs. JSON has a single number type. PHP has integers and floating-point numbers, and both are represented in JSON as number literals.

Preserve fractional zero

If you need 1.0 to stay 1.0, use JSONPRESERVEZERO_FRACTION.

<?php

$payload = [

‘taxRate‘ => 1.0,

‘total‘ => 100,

];

echo jsonencode($payload, JSONPRESERVEZEROFRACTION);

// {"taxRate":1.0,"total":100}

Numeric strings

JSONNUMERICCHECK converts numeric strings to numbers. This is risky if your IDs are numeric strings that must stay as strings.

<?php

$user = [

‘id‘ => ‘000123‘, // must remain string

‘balance‘ => ‘19.95‘,

];

echo jsonencode($user, JSONNUMERIC_CHECK);

// {"id":123,"balance":19.95}

In my experience, JSONNUMERICCHECK is only safe when you control the schema and you know all numeric-looking strings are meant to be numbers. Otherwise, convert explicitly in your domain layer.

When to use vs when NOT to use json_encode()

I use json_encode() for most JSON output in PHP, but there are times when I avoid it.

Use it when

  • You control the data shape and types.
  • You have a clear schema for your API or storage.
  • You need fast, lightweight serialization without extra dependencies.
  • You want predictable, stable JSON with explicit flags.

Avoid it when

  • You need advanced schema validation during serialization.
  • You require custom transformations for many nested objects and relations.
  • You need streaming output for very large datasets and want incremental encoding (use a streaming approach instead).
  • You rely on exact ordering of keys for cryptographic signing (you should canonicalize JSON separately).

In those cases, I move to a serializer layer or a streaming JSON writer instead of relying on json_encode() alone.

Common mistakes I see (and how you should avoid them)

  • Ignoring errors

I still see jsonencode() calls with no error checks. You should always handle errors explicitly or use JSONTHROWONERROR.

  • Unexpected object vs array output

Numeric array keys that aren’t sequential produce JSON objects. If your client expects arrays, reindex with array_values().

  • Double encoding

Encoding a value that already contains JSON as a string yields escaped JSON, not nested JSON. Decode first or store structure natively.

  • Invalid UTF-8

It fails in production when data sources are messy. Normalize early and decide on a policy for invalid bytes.

  • Using JSONNUMERICCHECK blindly

IDs and postal codes often look numeric but must remain strings. Convert intentionally.

  • Leaking internal object properties

If your object has public properties you don’t intend to expose, they’ll be encoded. Use JsonSerializable or DTOs.

Real-world scenarios and edge cases

Here are situations where I’ve seen json_encode() make or break a system.

API responses with typed contracts

If you publish an API, the shape of your JSON is part of your contract. I prefer explicit DTOs that are designed for the API, not domain entities that happen to work.

<?php

class UserProfile implements JsonSerializable

{

public function construct(

public string $id,

public string $displayName,

public string $email,

public ?string $avatarUrl,

) {}

public function jsonSerialize(): mixed

{

return [

‘id‘ => $this->id,

‘displayName‘ => $this->displayName,

‘email‘ => $this->email,

‘avatarUrl‘ => $this->avatarUrl,

];

}

}

$profile = new UserProfile(‘user_9382‘, ‘Jordan Lee‘, ‘[email protected]‘, null);

echo jsonencode($profile, JSONTHROWONERROR | JSONUNESCAPEDSLASHES);

JSON in logs

I store structured logs in JSON to make them searchable. I use JSONUNESCAPEDUNICODE for readability and include a stable schema.

<?php

$logEntry = [

‘timestamp‘ => date(‘c‘),

‘level‘ => ‘info‘,

‘event‘ => ‘payment_processed‘,

‘context‘ => [

‘invoiceId‘ => ‘inv2026001‘,

‘amount‘ => 149.95,

‘currency‘ => ‘USD‘,

],

];

echo jsonencode($logEntry, JSONTHROWONERROR | JSONUNESCAPEDUNICODE);

Embedding JSON in HTML

If you need to embed JSON into HTML, you should use JSONHEXTAG, JSONHEXAMP, JSONHEXAPOS, and JSONHEXQUOT to prevent injection issues.

<?php

$config = [

‘featureFlag‘ => true,

‘apiBase‘ => ‘https://example.com/api‘,

];

$flags = JSONHEXTAG JSONHEXAMP JSONHEXAPOSJSONHEXQUOT;

$encoded = jsonencode($config, $flags | JSONTHROWONERROR);

echo "window.APP_CONFIG = {$encoded};";

I treat this as a security boundary. If you can, avoid embedding JSON directly and use a data attribute or fetch it via an API.

Performance considerations and scale

json_encode() is fast for typical payloads, but you should still respect the size of your data. For payloads in the few kilobytes, latency is negligible. For tens of megabytes, encoding can become a noticeable part of request time.

Here’s how I keep encoding predictable:

  • Trim payloads: Avoid dumping entire ORM objects. Use DTOs to shape data.
  • Avoid circular references: If you accidentally reference the same object graph, you can hit recursion depth or memory spikes.
  • Chunk large datasets: For large exports, encode and stream chunks rather than a single giant array.

Typical encoding overhead for modest payloads lands in the single-digit to low tens of milliseconds. Once you climb into hundreds of megabytes, you’ll see memory pressure and longer encode time. At that point, I use streaming or export jobs rather than request-response APIs.

Traditional vs modern practices (2026 view)

Here’s how I compare older patterns with how I recommend doing it now.

Traditional approach

Modern approach in 2026

jsonencode($data) with no error checks

jsonencode($data, JSONTHROWONERROR) and explicit error handling

Directly encoding domain entities

DTOs or JsonSerializable with controlled output

Ignoring UTF-8 validity

Normalize input or enforce UTF-8 and use explicit policies

Large arrays encoded all at once

Streaming output for large exports

Manual JSON string building

Always use jsonencode() for correctnessI also integrate this with AI-assisted workflows: I generate JSON schema definitions from DTOs and run schema checks in CI. That way, I catch type drift early and keep encoded output stable for clients.

Testing strategies you can use today

I’ve found that JSON encoding bugs are easy to miss in unit tests because people often assert only that “output is not empty.” You should test shape and types.

Here’s a minimal example using PHPUnit-style assertions:

<?php

$payload = [

‘id‘ => ‘user_123‘,

‘score‘ => 99.5,

‘active‘ => true,

‘tags‘ => [‘beta‘, ‘vip‘],

];

$json = jsonencode($payload, JSONTHROWONERROR);

$decoded = jsondecode($json, true, 512, JSONTHROWONERROR);

// Shape checks

$this->assertIsArray($decoded);

$this->assertArrayHasKey(‘id‘, $decoded);

$this->assertArrayHasKey(‘score‘, $decoded);

$this->assertArrayHasKey(‘active‘, $decoded);

$this->assertArrayHasKey(‘tags‘, $decoded);

// Type checks

$this->assertIsString($decoded[‘id‘]);

$this->assertIsFloat($decoded[‘score‘]);

$this->assertIsBool($decoded[‘active‘]);

$this->assertIsArray($decoded[‘tags‘]);

I also add targeted tests for edge cases:

  • Invalid UTF-8 input should throw.
  • Numeric strings should stay strings when expected.
  • Arrays with non-sequential keys should encode as objects (or be normalized to arrays if that’s your contract).

When I test DTOs or JsonSerializable objects, I assert on the decoded output instead of the raw JSON string. That avoids brittle tests that fail because of key order or whitespace differences.

Deep dive: The flags you probably overlooked

There are a handful of flags that matter in specific scenarios. You don’t need all of them, but you should know when they exist.

JSONHEXTAG, JSONHEXAMP, JSONHEXAPOS, JSONHEXQUOT

These are your XSS defense flags when embedding JSON in HTML. They hex-encode characters like <, >, &, , and " so the JSON can’t escape a script or attribute context.

JSONINVALIDUTF8IGNORE and JSONINVALIDUTF8SUBSTITUTE

Use these only when you are okay with data loss or substitution. For logs and telemetry, I often use SUBSTITUTE to keep the payload structure while marking invalid bytes.

JSONPARTIALOUTPUTONERROR

This flag can be helpful in diagnostics, but dangerous in APIs. It will replace invalid values with null rather than failing the entire encode. That sounds helpful until your client depends on those values. I reserve this for logging or internal debugging.

JSONFORCEOBJECT

This forces arrays to encode as objects, even if they are lists. It’s useful when you need object output for a map but have numeric keys, but I’d rather fix the data structure and avoid forcing it globally.

JSONPRETTYPRINT

Pretty output is great for debugging and local development; it’s usually not worth the bytes in production responses. For APIs, I avoid it unless the payload is small and the API is primarily human-facing.

A reusable encoder class pattern

When you start to care about flags, error handling, and consistency, it’s natural to wrap json_encode() in a class. I use this approach when multiple services or packages need to share encoding rules.

<?php

final class JsonEncoder

{

private int $flags;

private int $depth;

public function construct(int $flags = 0, int $depth = 512)

{

$this->flags = $flags;

$this->depth = $depth;

}

public static function safeDefaults(): self

{

return new self(

JSONTHROWON_ERROR

| JSONUNESCAPEDUNICODE

| JSONUNESCAPEDSLASHES

| JSONPRESERVEZERO_FRACTION,

512

);

}

public function encode(mixed $value): string

{

return json_encode($value, $this->flags, $this->depth);

}

}

$encoder = JsonEncoder::safeDefaults();

$json = $encoder->encode([‘ok‘ => true]);

This looks like overkill until you maintain multiple apps or SDKs. Then it becomes a single point of truth for your JSON policy.

Object graphs and recursion: where things really break

json_encode() will recurse into arrays and objects. If your data has circular references, you’ll hit the depth limit or get a recursion error. This happens with ORM graphs and bidirectional relationships.

Here’s a simplified example:

<?php

class Node

{

public ?Node $parent = null;

public array $children = [];

public function construct(public string $name) {}

}

$root = new Node(‘root‘);

$child = new Node(‘child‘);

$child->parent = $root;

$root->children[] = $child;

// This will fail or hit depth issues

echo jsonencode($root, JSONTHROWONERROR);

The solution is to map to a JSON-friendly shape before encoding. For example, convert a graph to a tree or flatten references into IDs.

<?php

function nodetoarray(Node $node): array

{

return [

‘name‘ => $node->name,

‘children‘ => arraymap(fn (Node $child) => nodeto_array($child), $node->children),

];

}

echo jsonencode(nodetoarray($root), JSONTHROWONERROR);

This is another reason I prefer explicit DTOs for JSON. They force you to decide what the shape should be.

JSON for caching and storage

When you store JSON in a cache or database, you’re creating a long-lived contract for yourself. That means you should care about stability and backwards compatibility.

My approach:

  • Version your payloads: add a schemaVersion field when you store JSON for long periods.
  • Prefer explicit types: never rely on implicit conversions.
  • Use stable keys: even if your internal property names change, keep JSON keys stable.
<?php

$cacheItem = [

‘schemaVersion‘ => 1,

‘userId‘ => ‘user_9382‘,

‘features‘ => [‘beta_access‘ => true],

];

$cacheValue = jsonencode($cacheItem, JSONTHROWONERROR);

If you later change the schema, you can branch on schemaVersion when you decode old entries. That keeps caches resilient during migrations and feature rollouts.

JSON in queues and messaging systems

Queues and message brokers are one of the biggest reasons I pay attention to json_encode(). When you enqueue a message, you often don’t see it again until it fails in a worker.

I recommend:

  • Include a messageType and version field in every payload.
  • Fail fast on encoding errors so you don’t enqueue junk.
  • Keep messages small and focused; do not enqueue entire objects.
<?php

$message = [

‘messageType‘ => ‘user.welcome‘,

‘version‘ => 1,

‘payload‘ => [

‘userId‘ => ‘user_9382‘,

‘email‘ => ‘[email protected]‘,

],

];

$body = jsonencode($message, JSONTHROWONERROR);

If you maintain multiple services, this pattern lets you evolve messages without breaking older consumers.

Streaming output for large datasets

json_encode() requires the whole value in memory. That’s fine for small payloads but risky for large exports. If you need to export tens or hundreds of megabytes, I use a streaming strategy.

The idea: write a JSON array one item at a time. Here’s a simplified example:

<?php

function streamjsonarray(iterable $items): void

{

echo "[";

$first = true;

foreach ($items as $item) {

if (!$first) {

echo ",";

}

$first = false;

echo jsonencode($item, JSONTHROWONERROR);

}

echo "]";

}

This is not a drop-in replacement for all cases (and you should set correct headers in real responses), but it demonstrates the pattern. The key is to avoid building a massive array in memory.

Defensive JSON encoding in web frameworks

Most frameworks will call json_encode() under the hood when you return a JSON response. That’s convenient, but it can hide errors and flags. I often configure the framework’s JSON response behavior or wrap it.

A pattern I use:

  • Centralize JSON response creation with consistent flags.
  • Ensure JSONTHROWON_ERROR is used and exceptions are caught by a global error handler.
  • Standardize error payloads so clients can rely on a stable structure.

Example response shape:

<?php

$response = [

‘ok‘ => false,

‘error‘ => [

‘code‘ => ‘invalid_payload‘,

‘message‘ => ‘The request payload is malformed JSON.‘,

],

];

echo jsonencode($response, JSONTHROWONERROR);

When errors are consistent, clients can handle them reliably without guessing.

JSON encoding and security

json_encode() itself doesn’t sanitize malicious content. It just makes it JSON-safe. That means you still need to think about where the JSON ends up.

Key risks:

  • XSS when embedding JSON in HTML: use hex flags or separate API calls.
  • Log injection: if you log JSON to a plain text log, an attacker can inject new lines and confuse log parsers. Structured logging avoids this.
  • Data leakage: encoding full objects can expose sensitive internal fields. Explicit DTOs or JsonSerializable guard against this.

I consider encoding a security boundary: it’s the last point where I can reliably control what gets out.

Practical encoding recipes I actually reuse

Here are four small patterns I keep around and reuse across projects.

1) Encode and rethrow with context

<?php

function encodewithcontext(mixed $value, string $context): string

{

try {

return jsonencode($value, JSONTHROWONERROR);

} catch (JsonException $e) {

throw new RuntimeException("JSON encoding failed in {$context}: {$e->getMessage()}", 0, $e);

}

}

2) Force arrays for list outputs

<?php

function encode_list(array $items): string

{

return jsonencode(arrayvalues($items), JSONTHROWON_ERROR);

}

3) Encode for HTML embedding

<?php

function encodeforhtml(mixed $value): string

{

$flags = JSONHEXTAG JSONHEXAMP JSONHEXAPOS JSONHEXQUOT JSONTHROWON_ERROR;

return json_encode($value, $flags);

}

4) Encode with UTF-8 substitution for logs

<?php

function encodeforlog(mixed $value): string

{

$flags = JSONTHROWONERROR JSONUNESCAPEDUNICODE JSONINVALIDUTF8SUBSTITUTE;

return json_encode($value, $flags);

}

These helpers communicate intent and keep encoding consistent without retyping flags everywhere.

Edge cases worth knowing about

Some edge cases are rare but painful. Here are a few that have come up for me:

  • NAN and INF: json_encode() can’t represent NAN or INF in standard JSON. Depending on flags, it will fail. If you deal with unbounded math or metrics, normalize these values before encoding.
  • Empty arrays vs empty objects: an empty PHP array encodes as []. If your API expects {} for empty objects, you must build an object or use JSONFORCEOBJECT intentionally.
  • Binary strings: raw binary data should be base64-encoded before JSON. JSON is UTF-8 text; binary will break.
  • Large integers on 32-bit systems: in rare environments, large integers may become floats, losing precision. If exactness matters, encode big numbers as strings.

Alternative approaches when json_encode() is not enough

json_encode() is a great default, but you should know when to reach for other tools or patterns.

1) Schema-aware serializers

If you need validation, default values, or transformation during serialization, a schema-aware serializer can help. The tradeoff is more complexity and a heavier dependency.

2) JSON streaming libraries

For large exports or real-time feeds, streaming encoders can reduce memory pressure and latency. If your payloads are big enough to impact performance, it’s worth exploring.

3) Canonical JSON

If you must sign JSON for cryptographic verification, you need canonicalization: stable key ordering and exact representation. json_encode() doesn’t guarantee a canonical form. Use a canonical JSON library or build a deterministic ordering step before encoding.

Debugging JSON encoding failures quickly

When encoding fails, the most valuable thing is context. I use a checklist:

  • What is the top-level type?
  • What is the first invalid element?
  • Is there a UTF-8 issue?
  • Are there circular references?
  • Are there non-serializable resources (like file handles)?

A debugging helper can save time:

<?php

function debugjsonencode(mixed $value): string

{

try {

return jsonencode($value, JSONTHROWONERROR);

} catch (JsonException $e) {

$type = gettype($value);

throw new RuntimeException("JSON encoding failed (type={$type}): {$e->getMessage()}", 0, $e);

}

}

If you suspect a nested issue, try encoding subsets or use a recursive validator to locate the problem. This is especially useful for large arrays with mixed data.

A full, end-to-end API example

Here’s a complete example that shows the pattern I prefer: DTOs, safe encoding, and a consistent response format.

<?php

final class ApiResponse implements JsonSerializable

{

public function construct(

public bool $ok,

public mixed $data = null,

public ?array $error = null,

) {}

public function jsonSerialize(): mixed

{

return [

‘ok‘ => $this->ok,

‘data‘ => $this->data,

‘error‘ => $this->error,

];

}

}

final class UserDto implements JsonSerializable

{

public function construct(

public string $id,

public string $email,

public string $displayName,

) {}

public function jsonSerialize(): mixed

{

return [

‘id‘ => $this->id,

‘email‘ => $this->email,

‘displayName‘ => $this->displayName,

];

}

}

$user = new UserDto(‘user_9382‘, ‘[email protected]‘, ‘Jordan Lee‘);

$response = new ApiResponse(true, $user);

$json = jsonencode($response, JSONTHROWONERROR | JSONUNESCAPEDSLASHES);

echo $json;

This keeps the JSON shape explicit, reduces accidental leaks, and makes the encoding flags consistent. When I come back to the code months later, I can see exactly what the API returns.

Practical checklist for production JSON

When I review or build JSON encoding in a real system, I check the following:

  • JSONTHROWON_ERROR is used (or errors are handled explicitly).
  • Input is UTF-8 or normalized before encoding.
  • Arrays that must be lists are reindexed.
  • Domain entities are not encoded directly; DTOs or JsonSerializable are used.
  • Numeric strings are handled intentionally, not by JSONNUMERICCHECK by default.
  • Large payloads are streamed or chunked.
  • JSON embedding in HTML uses hex flags.
  • Encoded JSON is validated in tests by decoding and asserting types.

This checklist prevents 90% of the real bugs I’ve seen.

Why I treat json_encode() as part of my API design

When you expose JSON, you’re exposing behavior. Clients will build against your exact output. That means json_encode() is not just an implementation detail; it’s a contract.

That’s why I prefer:

  • Explicit JSON shapes via DTOs or JsonSerializable.
  • Consistent encoding flags.
  • Validation and testing that asserts types and shape.

If you approach json_encode() with that mindset, it becomes a reliable tool instead of a source of surprise.

Final takeaway

json_encode() looks simple, but it lives on a boundary that touches every system: APIs, logs, queues, caches, and UIs. Once you treat it as part of your public interface, you start to build safer patterns: explicit DTOs, predictable flags, error handling, and tests that validate shape and types.

If you only remember one thing, let it be this: encoding is not just serialization—it’s a contract.

Build that contract intentionally, and json_encode() will stay boring in the best possible way.

Scroll to Top