PHP 8.3: typed constants and choosing when to validate JSON

Strengthen class contracts with typed constants and Override, and avoid parsing JSON twice when decoding is already required.
Two boundaries worth making explicit
PHP 8.3 was released on 23 November 2023. Typed class constants strengthen contracts between classes, while json_validate() checks a JSON document without constructing its decoded representation. These are release-time features, not a reason to pin a new deployment to the original 8.3.0 release. Choose a maintained runtime and patch level according to your dependencies.
The useful question is where mistakes should become visible. An incompatible constant should fail when PHP checks the class definition. Invalid external JSON should produce a controlled input error. Valid JSON with the wrong business shape should fail a separate schema or field check. Treating those as one kind of validation makes error handling harder to reason about.
The files below were run on PHP 8.3.35 on 12 October 2026. Save each complete block under its displayed filename and run it with php followed by that path. They use the standard JSON extension and no framework. These are language examples, not a tested application upgrade.
Make inheritance promises executable
An interface can specify the type of a constant while allowing implementations to provide different values. The Override attribute separately checks that a method corresponds to a parent or interface method. It catches accidental renaming; it does not prove that the method implements the intended behavior.
<?php
declare(strict_types=1);
interface Formatter
{
public const string MEDIA_TYPE = 'application/octet-stream';
public function format(array $value): string;
}
final class JsonFormatter implements Formatter
{
public const string MEDIA_TYPE = 'application/json';
#[\Override]
public function format(array $value): string
{
return json_encode($value, JSON_THROW_ON_ERROR);
}
}
try {
echo JsonFormatter::MEDIA_TYPE, PHP_EOL;
echo (new JsonFormatter())->format(['enabled' => true]), PHP_EOL;
} catch (JsonException $error) {
fwrite(STDERR, 'The value cannot be encoded as JSON.' . PHP_EOL);
exit(1);
}
Validate syntax only when syntax is the result
Use json_validate() when the next step needs the original JSON text, for example checking a local fixture before forwarding it unchanged. It returns a boolean and exposes the parse error through json_last_error_msg(). It does not enforce required keys, value ranges, or an object at the root. The JSON value null is syntactically valid.
Do not call json_validate() immediately before json_decode() on the same input. That parses the document twice. If the application needs the decoded value, decode once with JSON_THROW_ON_ERROR and then check its shape. Set an input size limit before either operation; avoiding a decoded object does not make an arbitrarily large request harmless.
<?php
declare(strict_types=1);
function requireJsonDocument(string $json): string
{
if (strlen($json) > 65536) {
throw new InvalidArgumentException('JSON exceeds the 64 KiB limit.');
}
if (!json_validate($json, 64)) {
throw new InvalidArgumentException(json_last_error_msg());
}
return $json;
}
foreach (['{"enabled":true}', '{"enabled":}', 'null'] as $fixture) {
try {
echo requireJsonDocument($fixture), PHP_EOL;
} catch (InvalidArgumentException $error) {
echo 'Rejected: ', $error->getMessage(), PHP_EOL;
}
}
Keep syntax errors separate from domain errors
For a decoded settings object, require an object and a boolean enabled field explicitly. The example below distinguishes malformed JSON from a valid document with the wrong shape. Its error codes belong to this application, so callers need not depend on PHP’s parser wording.
The fixtures produce disabled for a boolean false, invalid_settings for the string "false", null and an array, and invalid_json for malformed syntax. The strict object check matters: decoding to an associative array would make an empty JSON object and an empty JSON array harder to distinguish.
When upgrading an existing package, adding constant types also raises its minimum parser requirement. Review the declared PHP constraint, development tooling, and CI matrix before publishing syntax that older consumers cannot parse. Adopt these features where the stronger contract is useful rather than treating every constant as migration work.
<?php
declare(strict_types=1);
function readEnabled(string $json): bool
{
if (strlen($json) > 65536) {
throw new InvalidArgumentException('payload_too_large');
}
try {
$settings = json_decode($json, false, 64, JSON_THROW_ON_ERROR);
} catch (JsonException) {
throw new InvalidArgumentException('invalid_json');
}
if (!$settings instanceof stdClass || !is_bool($settings->enabled ?? null)) {
throw new InvalidArgumentException('invalid_settings');
}
return $settings->enabled;
}
foreach (['{"enabled":false}', '{"enabled":"false"}', 'null', '[]', '{'] as $input) {
try {
echo readEnabled($input) ? "enabled\n" : "disabled\n";
} catch (InvalidArgumentException $error) {
echo $error->getMessage(), PHP_EOL;
}
}

