Skip to content

fix: detect @PropertyTransformer on read-only derived properties [DHIS2-21872]#24520

Merged
jason-p-pickering merged 2 commits into
masterfrom
fix/DHIS2-21872-propertytransformer-iswritable-gate
Jul 22, 2026
Merged

fix: detect @PropertyTransformer on read-only derived properties [DHIS2-21872]#24520
jason-p-pickering merged 2 commits into
masterfrom
fix/DHIS2-21872-propertytransformer-iswritable-gate

Conversation

@jason-p-pickering

Copy link
Copy Markdown
Contributor

Summary

PropertyPropertyIntrospector.updatePropertyTypes() only called initFromPropertyTransformerAnnotation(property) inside the if (property.isWritable()) branch. A property whose getter carries @PropertyTransformer but has no matching setter — e.g. UserRole.getUsers(), which is a computed getter derived from the separately-named members field (its own setter is setMembers(), there is no setUsers()) — is read-only, so the annotation was silently dropped from the schema. Property.hasPropertyTransformer() incorrectly returned false for UserRole.users at runtime, even though the annotation is right there in source.

Whether a property is serialized via a custom transformer is orthogonal to whether it can be written to, so this moves the annotation read outside the writability gate.

Other transformer-annotated properties happen to be unaffected because their getter has a matching setter (e.g. OrganisationUnit.users/setUsers(), UserGroup.members/setMembers()), so this specifically hit UserRole.users.

How this was found

While live-validating #24519 (DHIS2-21856, PropertyTransformer subtree pruning) against a local dev instance with Glowroot, GET /api/userRoles/{id}?fields=users[*] against a role with 83,385 members still took 139.7s / ~583k queries — unchanged from the pre-#24519 baseline. #24519 and the still-open #24514 both short-circuit on Property.hasPropertyTransformer(), but neither could work for UserRole.users specifically because this introspection bug meant the annotation was never seen in the first place.

With this fix applied (schema rebuilt, server restarted), the same request against the same role dropped to 5.76s.

Test plan

  • Added PropertyPropertyIntrospectorTest.detectsPropertyTransformerOnReadOnlyDerivedGetter, which runs the real JacksonPropertyIntrospectorPropertyPropertyIntrospector pipeline against the actual UserRole class (not a hand-built Property), so it exercises the exact reflection path that had the bug.
  • Verified the test fails against the pre-fix code (hasPropertyTransformer() false) and passes after the fix.
  • dhis-service-schema module test suite green (47/47).
  • Live-validated against local dev instance via Glowroot trace: 139.7s/~583k queries → 5.76s for the motivating request.

🤖 Generated with Claude Code

…S2-21872]

PropertyPropertyIntrospector.updatePropertyTypes() only read the
@PropertyTransformer annotation inside the isWritable() branch. A
property whose getter is annotated but has no matching setter (e.g.
UserRole.getUsers(), computed from the separately-named `members`
field) is read-only, so its annotation was silently dropped and
Property.hasPropertyTransformer() returned false at runtime even
though the annotation is present in source.

Move the annotation read outside the writability gate, since whether
a property is serialized via a custom transformer is orthogonal to
whether it can be written to.

Found while live-validating DHIS2-21856/PR #24519: with that fix
alone, GET /api/userRoles/{id}?fields=users[*] against an
83,385-member role still took 139.7s / ~583k queries because
hasPropertyTransformer() never saw UserRole.users' annotation. With
this fix applied, the same request drops to 5.76s.
…21872]

Per review feedback from @jbee: initFromPropertyAnnotation() was also
gated behind isWritable(), but most of @Property's attributes (type
override, required/persisted/owner flags, persistedAs aliasing)
describe the property itself and apply equally to read-only
properties or aliases, not just writable ones.

Snapshot writability before calling initFromPropertyAnnotation(),
since that call can itself flip it via access = READ_ONLY/WRITE_ONLY,
and range/min-max defaults should still be judged against the
pre-annotation writability (matches existing behavior for e.g.
Attribute.getObjectTypes(), which has a real setter overridden to
read-only via the annotation).

Added regression coverage: a read-only property now picks up
persistedAs() (previously silently ignored), and a writable property
whose annotation overrides it to read-only still gets range/min-max
defaults applied (unchanged behavior).
@sonarqubecloud

Copy link
Copy Markdown

@jason-p-pickering
jason-p-pickering merged commit fd1f288 into master Jul 22, 2026
25 checks passed
@jason-p-pickering
jason-p-pickering deleted the fix/DHIS2-21872-propertytransformer-iswritable-gate branch July 22, 2026 11:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants