PHP Safe JSON

Safe JSON parsing with exceptions, schema validation, and typed getters.
Requirements
Installation
composer require philiprehberger/php-safe-json
Usage
Decoding JSON
use PhilipRehberger\SafeJson\SafeJson;
$obj = SafeJson::decode('{"name":"Alice","age":30,"active":true}');
$obj->string('name'); // "Alice"
$obj->int('age'); // 30
$obj->bool('active'); // true
Dot Notation for Nested Access
$obj = SafeJson::decode('{"user":{"address":{"city":"Vienna"}}}');
$obj->string('user.address.city'); // "Vienna"
$obj->has('user.address.city'); // true
$obj->has('user.address.zip'); // false
Nested Objects
$obj = SafeJson::decode('{"user":{"name":"Alice"}}');
$user = $obj->object('user');
$user->string('name'); // "Alice"
Safe Decoding (No Exceptions)
$obj = SafeJson::tryDecode('{invalid}');
// Returns null instead of throwing
$obj = SafeJson::tryDecode('{"valid":true}');
// Returns JsonObject
Default Values
$obj = SafeJson::decode('{"name":"Alice"}');
$obj->get('name'); // "Alice"
$obj->get('missing', 'default'); // "default"
$obj->get('missing'); // throws JsonKeyException
Nullable Accessors
$obj = SafeJson::decode('{"name":"Alice","age":30}');
$obj->stringOrNull('name'); // "Alice"
$obj->stringOrNull('missing'); // null
$obj->intOrNull('name'); // null (wrong type)
$obj->intOrNull('age'); // 30
Merging Objects
$a = SafeJson::decode('{"name":"Alice","age":30}');
$b = SafeJson::decode('{"name":"Bob","email":"bob@example.com"}');
$merged = $a->merge($b);
$merged->string('name'); // "Bob" (overridden)
$merged->int('age'); // 30 (kept from $a)
$merged->string('email'); // "bob@example.com" (added from $b)
JSON Path Querying
$obj = SafeJson::decode('{"users":[{"name":"Alice","age":30},{"name":"Bob","age":25}]}');
$obj->query('$.users[*].name'); // ['Alice', 'Bob']
$obj->query('$.users[0].age'); // [30]
$obj->query('$..name'); // ['Alice', 'Bob'] (recursive descent)
$obj->query('$.users[0:1]'); // [['name' => 'Alice', 'age' => 30]]
Supported syntax: $ (root), .key (child), [0] (index), [*] (wildcard), ..key (recursive descent), [0:3] (slice), ['key'] (bracket notation).
JSON Diffing
$changes = SafeJson::diff(
'{"name":"Alice","age":30}',
'{"name":"Bob","age":30,"email":"bob@example.com"}'
);
// [
// ['op' => 'replace', 'path' => 'name', 'value' => 'Bob', 'old' => 'Alice'],
// ['op' => 'add', 'path' => 'email', 'value' => 'bob@example.com'],
// ]
Streaming Decode
// Memory-efficient decoding of large JSON arrays
foreach (SafeJson::decodeStream('/path/to/large-file.json') as $element) {
// Each element is decoded one at a time
// Only one element is held in memory
}
Encoding
$json = SafeJson::encode(['key' => 'value']);
// '{"key":"value"}'
$json = SafeJson::tryEncode($data);
// Returns null on failure instead of throwing
Serialization
$obj = SafeJson::decode('{"key":"value"}');
$obj->toArray(); // ['key' => 'value']
$obj->toJson(); // '{"key":"value"}'
json_encode($obj); // '{"key":"value"}' (JsonSerializable)
(string) $obj; // '{"key":"value"}' (Stringable)
API
SafeJson
| Method | Description |
|---|
decode(string $json): JsonObject | Decode JSON string, throws JsonDecodeException on failure |
tryDecode(string $json): ?JsonObject | Decode JSON string, returns null on failure |
encode(mixed $data, int $flags = 0): string | Encode to JSON string, throws on failure |
tryEncode(mixed $data, int $flags = 0): ?string | Encode to JSON string, returns null on failure |
diff(string $jsonA, string $jsonB): array | Compare two JSON strings and return differences |
decodeStream(string $filePath): Generator | Stream-decode a JSON array file, yielding elements one at a time |
JsonObject
| Method | Description |
|---|
string(string $key): string | Get string value by key |
int(string $key): int | Get integer value by key |
float(string $key): float | Get float value by key (accepts integers) |
bool(string $key): bool | Get boolean value by key |
stringOrNull(string $key): ?string | Get string value or null if missing/wrong type |
intOrNull(string $key): ?int | Get integer value or null if missing/wrong type |
floatOrNull(string $key): ?float | Get float value or null if missing/wrong type |
boolOrNull(string $key): ?bool | Get boolean value or null if missing/wrong type |
array(string $key): array | Get array value by key |
object(string $key): JsonObject | Get nested JsonObject by key |
get(string $key, mixed $default = null): mixed | Get value without type enforcement |
has(string $key): bool | Check if key exists |
merge(self $other): self | Merge with another JsonObject (other overrides on conflict) |
query(string $path): array | Query data using JSON Path expression |
toArray(): array | Return underlying array |
toJson(int $flags = 0): string | Return JSON string |
All key-based methods support dot notation for nested access (e.g., user.address.city).
JsonPath
| Method | Description |
|---|
query(array $data, string $path): array | Query data using JSON Path expression |
JsonDiff
| Method | Description |
|---|
diff(mixed $a, mixed $b, string $path = ''): array | Compare two values and return list of differences |
StreamDecoder
| Method | Description |
|---|
decodeStream(string $filePath): Generator | Stream-decode a JSON array file element by element |
Exceptions
| Exception | Thrown When |
|---|
JsonDecodeException | Invalid JSON input |
JsonEncodeException | Failed to encode data |
JsonKeyException | Missing key or type mismatch |
Development
composer install
vendor/bin/phpunit
vendor/bin/pint --test
vendor/bin/phpstan analyse
Support
If you find this project useful:
⭐ Star the repo
🐛 Report issues
💡 Suggest features
❤️ Sponsor development
🌐 All Open Source Projects
💻 GitHub Profile
🔗 LinkedIn Profile
License
MIT