Skip to content

Fix GH-25108: Skip uninitialized hooked properties in json_encode() - #25110

Open
JoaoMarcos44 wants to merge 1 commit into
php:PHP-8.4from
JoaoMarcos44:fix/json-uninitialized-hooks-84
Open

JoaoMarcos44 wants to merge 1 commit into
php:PHP-8.4from
JoaoMarcos44:fix/json-uninitialized-hooks-84

Conversation

@JoaoMarcos44

@JoaoMarcos44 JoaoMarcos44 commented Oct 31, 2026 •

Copy link
Copy Markdown

Fixes #25108.

AI-assisted contribution. The explanation, patch, and regression test were prepared with AI assistance. The reported reproductions, build results, and test counts were checked by execution.

Summary

Adding a setter hook to an uninitialized backed property changes its JSON representation from an omitted field to an explicit null. This patch skips the existing uninitialized-value sentinel before the encoder writes the property.

Infographic

A shrine archivist omits a fading uninitialized vessel but records a solid null vessel.

AI-generated conceptual illustration: the JSON encoder omits an uninitialized hooked property but retains an explicitly initialized null or a null returned by a getter. This is not a screenshot or test evidence.

The botanical illustration distinguishes missing data from an explicit null value. It is an AI-generated diagram, not a screenshot of test output.

Minimal reproduction

<?php
class Plain {
    public int $id;
}
class WithSetter {
    public int $id {
        set => $value;
    }
}

echo json_encode(new Plain()), "\n";
echo json_encode(new WithSetter()), "\n";
Object Before the fix Expected and after the fix
Ordinary uninitialized property {} {}
Uninitialized property with setter hook {"id":null} {}

A setter hook should not make an uninitialized field look initialized. Applications may distinguish an absent field from an explicit null in serialized data.

Root cause

The encoder reads hooked properties in silent mode. An uninitialized backed property returns EG(uninitialized_zval). The encoder previously accepted that marker as a value and emitted the property as null. The ordinary-property path already omits uninitialized fields.

Small implementation

if (UNEXPECTED(data == &EG(uninitialized_zval))) {
    zval_ptr_dtor(&tmp);
    continue;
}

The guard checks the identity of the uninitialized sentinel, not whether a value is null. A real nullable property initialized to null, or a getter returning null, must still be encoded. The existing exception check runs first. The temporary value is released before continuing.

The guard runs before writing the property name or separator, so omitted fields do not leave extra commas. It uses the existing loop, cleanup operation, and sentinel. No new helper, option, or object model is introduced.

The implementation is in json_encoder.c. The commit also adds a PHPT and a NEWS entry.

Regression coverage

gh25108.phpt covers the following observable behavior:

Case Expected behavior
Uninitialized ordinary or hooked typed property Omitted
Uninitialized hooked nullable property Omitted
Initialized hooked value Included with its value
Backed nullable property explicitly initialized to null Included as null
Getter returning null Included as null
Virtual set-only property Omitted
Missing fields before, between, and after visible fields Remaining order and separators are correct
Compact and pretty-printed JSON Both formats remain valid

Validation

The target is PHP-8.4, the lowest actively supported affected branch. Validation used PHP 8.4.28-dev on Linux with --enable-debug --enable-zts, based on dfcc0433fcda7fb732cba8d7ee8c857d23f9a9e7.

make -j1 sapi/cli/php
export NO_INTERACTION=1
export REPORT_EXIT_STATUS=1
make -j1 test TESTS='-q -j1 --no-color --no-progress --show-diff ext/json/tests/gh25108.phpt'
make -j1 test TESTS='-q -j1 --no-color --no-progress --show-diff -g FAIL,BORK,WARN,LEAK,SKIP ext/json/tests Zend/tests/property_hooks'

Before the fix, the regression produced unwanted null-valued fields. After the fix, it passed 1 of 1. The JSON and property-hook suites reported 296 tests: 291 passed and 5 skipped, with no failures or warnings. Three skips require zend_test; two require unavailable locales. The build and corrected validator exited 0.

Scope and limits

Public signatures and JSON options are unchanged. Actual null values remain distinct from uninitialized properties. The complete PHP suite and Windows builds were not run. Local results do not imply that remote CI has completed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant