Skip to content

Newly-added readable property on an existing resource is silently dropped from the (JSON-LD) output until var/cache is deleted; cache:clear does not invalidate it #8332

Description

@soyuka

Summary

After adding a readable property to an already-cached API resource class, the serialized
output (tested with application/ld+json) omits that property — no error, the key is simply
absent — even though the property is readable: true, has a value set, and shows up in the
OpenAPI/Hydra schema. Deleting var/cache/<env> fixes it; bin/console cache:clear and
cache:pool:clear do not.

The metadata is correct when computed fresh. The drop happens because the serializer reads a
stale PropertyNameCollection out of api_platform.cache.metadata.property.

Environment

  • Symfony 7.x, PHP 8.5, debug enabled (dev and test reproduce identically).
  • Note (transparency): the install where this was found is version-skewed
    api-platform/metadata + api-platform/json-schema at 4.3.13, the rest
    (serializer, jsonld, hydra, state, symfony, …) at 4.3.10. I was not able to fully
    rule the skew out as a contributing factor, but the mechanism below is version-independent.

Reproduction

  1. A custom (non-Doctrine) resource backed by a state provider, e.g. a DTO with
    #[ApiProperty(writable: false)] public int $foo = 0; set by the provider. Hit the item GET
    once so the metadata is cached.
  2. Add another readable property to the same existing class
    (#[ApiProperty(writable: false)] public int $bar = 0;), set it in the provider/fromEntity.
  3. Hit the item GET again.

Expected: bar present. Actual: bar absent, no error.

rm -rf var/cache/<env>bar appears. bin/console cache:clear (or
cache:pool:clear cache.system) → bar still absent.

Where the drop happens (traced)

  • PropertyNameCollectionFactoryInterface::create() / PropertyMetadataFactoryInterface::create()
    called directly return the full, fresh set: bar present, readable=true, native type
    resolved. Metadata layer is fine.
  • AbstractItemNormalizer::getAllowedAttributes()
    $this->propertyNameCollectionFactory->create($resourceClass, $options) returns a stale
    list that does not include bar. So the new readable property never reaches the attribute set;
    it is dropped with no error. (getFactoryOptions() builds slightly different options than a
    hand call, but the relevant difference is just that the serializer's call hits a cached entry.)

Mechanism

  • api_platform.cache.metadata.property is defined ->parent('cache.system') (a PhpFilesAdapter
    under var/cache/<env>/pools/system/<ns>/). Its items are versioned by container.build_id
    (AbstractAdapter::createSystemCache('<ns>', 0, $container->getParameter('container.build_id'), …)).
  • The cached PropertyNameCollection key is derived from resource class + options — it carries
    no source-version component. So once the collection for a class is cached, a body edit to
    that class (adding a property) does not change the key, and the cached value keeps being served.
  • api_platform.cache_warmer.cache_pool_clearer (registered only in debug) is supposed to clear
    these pools on warmup, yet in practice cache:clear does not invalidate the entry the running
    app/worker reads — only physically deleting var/cache does. (I could not make the
    container.build_id / pool-namespace / rebuild-timing interaction deterministic enough to say
    exactly why the clearer misses; that part may be Symfony cache.system plumbing rather than core.)

Impact

  • Under a long-lived worker (FrankenPHP/Symfony runtime) it is worse: metadata is read once at
    boot and a restart re-reads the same stale persistent pool. var/cache must be wiped before the
    restart.

Notes

Adding a new resource class (which changes the compiled container) is picked up fine; the bug
is specifically editing the property set of an existing, already-cached resource. Happy to
build a minimal reproduction app if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions