@@ -125,6 +125,68 @@ Access control checks in the `security` attribute are always executed before the
125125` object` doesn't contain the value submitted by the user, but values currently stored in
126126[the persistence layer](../core/state-processors.md).
127127
128+ # # Property-Level Security on Write Operations
129+
130+ When `security` is used on `#[ApiProperty]`, the behavior during write operations (PATCH, PUT) differs
131+ from what you might expect based on the operation-level behavior described above.
132+
133+ During deserialization, `security` on a property is evaluated to build the list of allowed attributes
134+ **before the object is populated with incoming data**. At that point, no object instance is available
135+ yet, so the `object` variable is always `null`. This is by design : ` security` on `ApiProperty` acts
136+ as a static, context-free gate — it answers "is this user allowed to interact with this field at all?"
137+ independently of the current object state.
138+
139+ ` ` ` php
140+ <?php
141+ // api/src/Entity/Book.php
142+ namespace App\E ntity;
143+
144+ use ApiPlatform\M etadata\A piProperty;
145+
146+ class Book
147+ {
148+ // object is null during PATCH/PUT — use only role checks or user checks here
149+ #[ApiProperty(security: "is_granted('ROLE_ADMIN')")]
150+ private ?string $adminOnlyField = null;
151+ }
152+ ` ` `
153+
154+ If your access logic depends on the object's current state (for example, to restrict changes based
155+ on ownership or a previous value), use `securityPostDenormalize` instead. It runs after the incoming
156+ data has been applied to the object, and provides both `object` (the updated entity) and
157+ ` previous_object` (a clone of the entity before the write).
158+
159+ When the `securityPostDenormalize` check fails for a property, the value is **silently reverted** to
160+ its previous value (from `previous_object`) rather than producing an error. The request still
161+ succeeds for any other properties the user is permitted to modify. To instead return a 403 response,
162+ set the `throw_on_access_denied` extra property on the operation or on the individual property
163+ (see the section below).
164+
165+ ` ` ` php
166+ <?php
167+ // api/src/Entity/Book.php
168+ namespace App\E ntity;
169+
170+ use ApiPlatform\M etadata\A piProperty;
171+
172+ class Book
173+ {
174+ // Combines a static role check (security) with an object-aware ownership check (securityPostDenormalize)
175+ #[ApiProperty(
176+ security: "is_granted('ROLE_EDITOR')",
177+ securityPostDenormalize: "is_granted('FLAVOR_EDIT', object)"
178+ )]
179+ private ?string $flavor = null;
180+ }
181+ ` ` `
182+
183+ In this example :
184+
185+ - ` security` blocks users without `ROLE_EDITOR` from writing the property entirely (evaluated
186+ before denormalization, `object` is `null`)
187+ - ` securityPostDenormalize` further restricts writes to users granted `FLAVOR_EDIT` on the
188+ **updated** object (evaluated after denormalization, `object` is the populated entity)
189+
128190# # Executing Access Control Rules After Denormalization
129191
130192In some cases, it might be useful to execute a security after the denormalization step. To do so,
0 commit comments