Skip to content

[NAE-2451] Optimize and Reduce Outcome Size #462

Open
renczesstefan wants to merge 7 commits into
release/7.0.0from
NAE-2451
Open

[NAE-2451] Optimize and Reduce Outcome Size #462
renczesstefan wants to merge 7 commits into
release/7.0.0from
NAE-2451

Conversation

@renczesstefan

@renczesstefan renczesstefan commented Jul 9, 2026

Copy link
Copy Markdown
Member

Description

Optimized outcome size that is returned to frontend.

Implements NAE-2451

Dependencies

No new dependencies were introduced

Third party dependencies

No new dependencies were introduced

Blocking Pull requests

There are no dependencies on other PR

How Has Been This Tested?

This was tested manually and with unit tests

Test Configuration

Name Tested on
OS macOS Tahoe 26.3
Runtime Java 21
Dependency Manager Maven 3.9.9n
Framework version Spring Boot 3.4.4
Run parameters
Other configuration

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • My changes have been checked, personally or remotely, with @machacjozef
  • I have commented my code, particularly in hard-to-understand areas
  • I have resolved all conflicts with the target branch of the PR
  • I have updated and synced my code with the target branch
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing tests pass locally with my changes:
    • Lint test
    • Unit tests
    • Integration tests
  • I have checked my contribution with code analysis tools:
  • I have made corresponding changes to the documentation:
    • Developer documentation
    • User Guides
    • Migration Guides

Summary by CodeRabbit

  • New Features

    • Added support for selecting which response fields are returned via a query parameter.
    • Introduced smarter JSON output that can include only requested nested fields.
    • Updated event outcome responses to support partial serialization and omit empty values.
  • Bug Fixes

    • Improved request handling so field-selection settings are applied consistently for each incoming request.

…nt outcomes

Added `FieldSelector` and related classes (`FieldSelectorHolder`, `FieldSelectorInterceptor`, `ChangeRecordSerializer`, and `ObjectMapperConfiguration`) to enable selective field serialization based on request parameters. Integrated the interceptor into `WebConfig` for seamless request handling.
Introduced `DynamicFieldSerializer` to enable nested field selection during serialization. Updated `ChangeRecordSerializer` to leverage `DynamicFieldSerializer` for handling complex object structures with field selectors. Enhanced `FieldSelector` functionality to support nested field inclusion checks.
Renamed `ChangeRecordSerializer` to `LocalizedEventOutcomeSerializer` for better clarity and alignment with its purpose and usage in serializing `LocalisedEventOutcome`. Updated references in `ObjectMapperConfiguration`.
Enhanced JavaDocs for `FieldSelector`, `FieldSelectorHolder`, `FieldSelectorInterceptor`, `LocalizedEventOutcomeSerializer`, and `DynamicFieldSerializer`. Improved clarity and provided detailed explanations of class functionalities, methods, and usage scenarios to support selective field serialization.
Enhanced handling of blank or null specifications, added trimming for field names, and ensured empty names are excluded during parsing.
- Refactor serialization components to replace `JsonSerializer` with `ValueSerializer` and update `ObjectMapper` configuration for improved compatibility.
@renczesstefan renczesstefan self-assigned this Jul 9, 2026
@renczesstefan renczesstefan marked this pull request as ready for review July 9, 2026 10:35
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR adds a field-selection mechanism for JSON serialization: a FieldSelector parser for comma/parenthesis-based field specs, a request-scoped FieldSelectorHolder, a FieldSelectorInterceptor populating it from a "fields" query parameter, a generic DynamicFieldSerializer, an outcome-specific LocalizedEventOutcomeSerializer, and wiring via WebConfig and ObjectMapperConfiguration.

Changes

Dynamic field selection for JSON serialization

Layer / File(s) Summary
FieldSelector parsing and query logic
application-engine/.../serialization/FieldSelector.java
New class parsing comma/parenthesis field specs into fields/nested maps, with parse(), includes(), includeAll(), nested(), and comma/parenthesis-matching helpers.
Request-scoped holder and interceptor
application-engine/.../serialization/FieldSelectorHolder.java, application-engine/.../serialization/FieldSelectorInterceptor.java
Request-scoped component storing the active FieldSelector, and an interceptor that parses the "fields" query parameter into it on each request.
DynamicFieldSerializer generic bean serialization
application-engine/.../serialization/DynamicFieldSerializer.java
Jackson ValueSerializer that filters bean properties by selector inclusion, recursing into nested selectors and delegating to default serialization when unrestricted.
LocalizedEventOutcomeSerializer conditional outcome fields
application-engine/.../serialization/LocalizedEventOutcomeSerializer.java
Serializer for LocalisedEventOutcome that conditionally writes message, frontActions, outcomes, and type-specific nested payloads based on selector inclusion.
Spring MVC and Jackson wiring
application-engine/.../configuration/web/config/WebConfig.java, application-engine/.../serialization/ObjectMapperConfiguration.java
WebConfig registers FieldSelectorInterceptor via WebMvcConfigurer; ObjectMapperConfiguration registers the outcome serializer and sets NON_NULL inclusion.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant FieldSelectorInterceptor
  participant FieldSelectorHolder
  participant LocalizedEventOutcomeSerializer
  participant DynamicFieldSerializer

  Client->>FieldSelectorInterceptor: HTTP request with "fields" query param
  FieldSelectorInterceptor->>FieldSelectorInterceptor: FieldSelector.parse(fields)
  FieldSelectorInterceptor->>FieldSelectorHolder: setSelector(parsedSelector)
  Client->>LocalizedEventOutcomeSerializer: serialize(outcome)
  LocalizedEventOutcomeSerializer->>FieldSelectorHolder: getSelector()
  LocalizedEventOutcomeSerializer->>LocalizedEventOutcomeSerializer: write message/frontActions/outcomes if included
  LocalizedEventOutcomeSerializer->>DynamicFieldSerializer: serializeWithSelector(nestedValue, nestedSelector)
  DynamicFieldSerializer->>DynamicFieldSerializer: filter bean properties by selector.includes()
  DynamicFieldSerializer-->>LocalizedEventOutcomeSerializer: JSON written
  LocalizedEventOutcomeSerializer-->>Client: JSON response
Loading

Suggested labels: improvement, Large

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately reflects the main change: reducing outcome size through serialization optimization.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot added improvement A change that improves on an existing feature Large labels Jul 9, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@application-engine/src/main/java/com/netgrif/application/engine/serialization/DynamicFieldSerializer.java`:
- Around line 108-124: The DynamicFieldSerializer#serializeWithSelector loop can
throw a NullPointerException when an included property value is null because the
nested.includeAll() branch calls propValue.getClass() without a null check.
Update the handling around prop.getAccessor().getValue(value) so the includeAll
path serializes null values safely, reusing the method’s existing null handling
logic before calling context.findValueSerializer, and keep the fix localized to
serializeWithSelector and its nested.includeAll() branch.
- Around line 3-4: The Javadocs in DynamicFieldSerializer are still using
Jackson 2 terminology, and the Jackson 2 imports are no longer needed since
those types are only mentioned in comments. Update the doc references in
serializeWithSelector(...) and related Javadocs to use ValueSerializer and
SerializationContext instead of JsonSerializer and SerializerProvider, and
remove the unused Jackson 2 imports from the class.

In
`@application-engine/src/main/java/com/netgrif/application/engine/serialization/FieldSelector.java`:
- Around line 85-113: `FieldSelector.parse()` is recursively parsing nested
field specs without any depth limit, so a crafted `fields` value can trigger
`StackOverflowError`. Add a recursion-depth guard to `parse()` (and its nested
`parse(inner)` call path), using a bounded counter or maximum nesting constant,
and reject overly deep specs before recursing further. Use the existing `parse`,
`findMatchingParen`, and `nextCommaOrEnd` flow to keep the change localized.

In
`@application-engine/src/main/java/com/netgrif/application/engine/serialization/FieldSelectorHolder.java`:
- Around line 1-36: The field-selection serializer setup is incomplete because
the SimpleModule containing LocalizedEventOutcomeSerializer is created but never
actually registered with the Jackson builder. Update the customizer that builds
the ObjectMapper so the module is explicitly attached before returning it,
rather than relying on findAndAddModules(); use the existing module-building
logic around LocalizedEventOutcomeSerializer and ensure it is added to the
builder so FieldSelectorHolder-driven selection takes effect.

In
`@application-engine/src/main/java/com/netgrif/application/engine/serialization/FieldSelectorInterceptor.java`:
- Around line 18-22: The FieldSelectorInterceptor.preHandle path lets
FieldSelector.parse() throw IllegalArgumentException for malformed fields input,
which bubbles up as a 500. Update preHandle() to catch parse failures around
holder.setSelector(FieldSelector.parse(fields)) and reject the request with a
400 Bad Request instead of allowing the exception to escape; use
FieldSelector.parse and FieldSelectorInterceptor as the key places to change.

In
`@application-engine/src/main/java/com/netgrif/application/engine/serialization/LocalizedEventOutcomeSerializer.java`:
- Around line 99-101: The `LocalizedEventOutcomeSerializer` case branch is using
the selector key `case` while emitting the JSON field name `aCase`, which is
inconsistent with the other fields. Update the `outcome instanceof
LocalisedCaseEventOutcome` handling so the selector check, nested selector, and
written property name all use the same identifier (matching the actual output
name used elsewhere in the serializer), and keep
`dynamicSerializer.serializeWithSelector` aligned with that same symbol.

In
`@application-engine/src/main/java/com/netgrif/application/engine/serialization/ObjectMapperConfiguration.java`:
- Around line 17-19: The ObjectMapperConfiguration change is too broad because
`changeDefaultPropertyInclusion` makes `NON_NULL` apply globally instead of only
to `LocalisedEventOutcome`. Update `ObjectMapperConfiguration` so the
null-handling is scoped to `LocalisedEventOutcome` (or register the
`SimpleModule` separately for `LocalizedEventOutcomeSerializer`) and keep the
mapper’s default inclusion behavior unchanged for all other responses.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e4a8b2c2-0d9b-4dee-902a-dfa57f6a37d7

📥 Commits

Reviewing files that changed from the base of the PR and between 3d8cccd and b68cae7.

📒 Files selected for processing (7)
  • application-engine/src/main/java/com/netgrif/application/engine/configuration/web/config/WebConfig.java
  • application-engine/src/main/java/com/netgrif/application/engine/serialization/DynamicFieldSerializer.java
  • application-engine/src/main/java/com/netgrif/application/engine/serialization/FieldSelector.java
  • application-engine/src/main/java/com/netgrif/application/engine/serialization/FieldSelectorHolder.java
  • application-engine/src/main/java/com/netgrif/application/engine/serialization/FieldSelectorInterceptor.java
  • application-engine/src/main/java/com/netgrif/application/engine/serialization/LocalizedEventOutcomeSerializer.java
  • application-engine/src/main/java/com/netgrif/application/engine/serialization/ObjectMapperConfiguration.java

Comment on lines +3 to +4
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

🧩 Analysis chain

🌐 Web query:

tools.jackson.databind SerializationContext introspectBeanDescription Jackson 3 API

💡 Result:

In the Jackson 3 API, the introspectBeanDescription method is defined within the SerializationContext class [1]. However, it is explicitly intended for internal framework use rather than as a public API for general application code [2]. Jackson maintainers have clarified that the introspection mechanism used by internal serializers is not designed to be exposed for external use [2]. While the method may appear in the SerializationContext documentation, it is typically considered restricted to testing or internal processing [2]. For tasks requiring external introspection of types or bean properties, the recommended public API approach is to use ObjectMapper.acceptJsonFormatVisitor() [2]. This method is the officially supported way to inspect or interact with the serialization handling logic of a type [2]. For more advanced requirements, SerializationContextExt is available as an extension of SerializationContext that exposes additional methods needed specifically by ObjectMapper and ObjectWriter, which are further decoupled from the standard SerializationContext API to prevent accidental misuse [3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="application-engine/src/main/java/com/netgrif/application/engine/serialization/DynamicFieldSerializer.java"

echo "== file exists =="
test -f "$file" && echo yes

echo
echo "== line-numbered excerpt =="
cat -n "$file" | sed -n '1,220p'

echo
echo "== dependency/version hints =="
rg -n --hidden --glob '!**/target/**' --glob '!**/build/**' \
  'tools\.jackson|com\.fasterxml\.jackson|jackson-databind|jackson-core' \
  pom.xml **/pom.xml build.gradle* settings.gradle* gradle.properties 2>/dev/null || true

Repository: netgrif/application-engine

Length of output: 7136


Update the Javadocs to Jackson 3. JsonSerializer/SerializerProvider are only referenced in doc comments, and serializeWithSelector(...) still documents the old SerializerProvider signature. Switch those references to ValueSerializer/SerializationContext and drop the Jackson 2 imports.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@application-engine/src/main/java/com/netgrif/application/engine/serialization/DynamicFieldSerializer.java`
around lines 3 - 4, The Javadocs in DynamicFieldSerializer are still using
Jackson 2 terminology, and the Jackson 2 imports are no longer needed since
those types are only mentioned in comments. Update the doc references in
serializeWithSelector(...) and related Javadocs to use ValueSerializer and
SerializationContext instead of JsonSerializer and SerializerProvider, and
remove the unused Jackson 2 imports from the class.

Comment on lines +108 to +124
for (BeanPropertyDefinition prop : desc.findProperties()) {

String name = prop.getName();
if (!selector.includes(name)) {
continue;
}

Object propValue = prop.getAccessor().getValue(value);
gen.writeName(name);

FieldSelector nested = selector.nested(name);
if (nested.includeAll()) {
context.findValueSerializer(propValue.getClass()).serialize(propValue, gen, context);
} else {
serializeWithSelector(propValue, gen, context, nested);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

NPE when an included property value is null.

propValue can be null. In the nested.includeAll() branch (Line 120) you call propValue.getClass(), which throws a NullPointerException. This is reachable for any included leaf field whose value is null (the common case, since leaf fields have no nested selector and thus take the includeAll() path). The recursive else branch handles null via the top-of-method guard, but the includeAll branch does not.

🐛 Proposed fix to handle null property values
             Object propValue = prop.getAccessor().getValue(value);
             gen.writeName(name);
 
+            if (propValue == null) {
+                gen.writeNull();
+                continue;
+            }
+
             FieldSelector nested = selector.nested(name);
             if (nested.includeAll()) {
                 context.findValueSerializer(propValue.getClass()).serialize(propValue, gen, context);
             } else {
                 serializeWithSelector(propValue, gen, context, nested);
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (BeanPropertyDefinition prop : desc.findProperties()) {
String name = prop.getName();
if (!selector.includes(name)) {
continue;
}
Object propValue = prop.getAccessor().getValue(value);
gen.writeName(name);
FieldSelector nested = selector.nested(name);
if (nested.includeAll()) {
context.findValueSerializer(propValue.getClass()).serialize(propValue, gen, context);
} else {
serializeWithSelector(propValue, gen, context, nested);
}
}
for (BeanPropertyDefinition prop : desc.findProperties()) {
String name = prop.getName();
if (!selector.includes(name)) {
continue;
}
Object propValue = prop.getAccessor().getValue(value);
gen.writeName(name);
if (propValue == null) {
gen.writeNull();
continue;
}
FieldSelector nested = selector.nested(name);
if (nested.includeAll()) {
context.findValueSerializer(propValue.getClass()).serialize(propValue, gen, context);
} else {
serializeWithSelector(propValue, gen, context, nested);
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@application-engine/src/main/java/com/netgrif/application/engine/serialization/DynamicFieldSerializer.java`
around lines 108 - 124, The DynamicFieldSerializer#serializeWithSelector loop
can throw a NullPointerException when an included property value is null because
the nested.includeAll() branch calls propValue.getClass() without a null check.
Update the handling around prop.getAccessor().getValue(value) so the includeAll
path serializes null values safely, reusing the method’s existing null handling
logic before calling context.findValueSerializer, and keep the fix localized to
serializeWithSelector and its nested.includeAll() branch.

Comment on lines +85 to +113
public static FieldSelector parse(String spec) {
if (spec == null || spec.isBlank()) {
return new FieldSelector(null, null);
}

Set<String> fields = new HashSet<>();
Map<String, FieldSelector> nested = new HashMap<>();

int i = 0;
while (i < spec.length()) {
int commaIdx = nextCommaOrEnd(spec, i);
int parenthesisIdx = spec.indexOf('(', i);

if (parenthesisIdx != -1 && parenthesisIdx < commaIdx) {
String name = spec.substring(i, parenthesisIdx).trim();
int closeParenthesisIdx = findMatchingParen(spec, parenthesisIdx);
String inner = spec.substring(parenthesisIdx + 1, closeParenthesisIdx);
fields.add(name);
nested.put(name, parse(inner));
i = closeParenthesisIdx + 1;
if (i < spec.length() && spec.charAt(i) == ',') i++;
} else {
String name = spec.substring(i, commaIdx).trim();
if (!name.isEmpty()) fields.add(name);
i = commaIdx + 1;
}
}
return new FieldSelector(fields, nested);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Unbounded recursion in parse() allows StackOverflowError from user-controlled input.

The fields query parameter is user-controlled and parse() recurses without a depth limit. A crafted specification with thousands of nesting levels (e.g., a(a(a(a(...))))) can exhaust the call stack and crash the request-handling thread. With typical Tomcat URL limits (~8KB) and Java's default stack size (~512KB), this is achievable in a single GET request.

🔒 Proposed fix: add a depth guard
 public static FieldSelector parse(String spec) {
+    return parse(spec, 0);
+}
+
+private static final int MAX_DEPTH = 32;
+
+private static FieldSelector parse(String spec, int depth) {
+    if (depth >= MAX_DEPTH) {
+        throw new IllegalArgumentException("Field selector nesting exceeds maximum depth of " + MAX_DEPTH);
+    }
     if (spec == null || spec.isBlank()) {
         return new FieldSelector(null, null);
     }
 
     Set<String> fields = new HashSet<>();
     Map<String, FieldSelector> nested = new HashMap<>();
 
     int i = 0;
     while (i < spec.length()) {
         int commaIdx = nextCommaOrEnd(spec, i);
         int parenthesisIdx = spec.indexOf('(', i);
 
         if (parenthesisIdx != -1 && parenthesisIdx < commaIdx) {
             String name = spec.substring(i, parenthesisIdx).trim();
             int closeParenthesisIdx = findMatchingParen(spec, parenthesisIdx);
             String inner = spec.substring(parenthesisIdx + 1, closeParenthesisIdx);
             fields.add(name);
-            nested.put(name, parse(inner));
+            nested.put(name, parse(inner, depth + 1));
             i = closeParenthesisIdx + 1;
             if (i < spec.length() && spec.charAt(i) == ',') i++;
         } else {
             String name = spec.substring(i, commaIdx).trim();
             if (!name.isEmpty()) fields.add(name);
             i = commaIdx + 1;
         }
     }
     return new FieldSelector(fields, nested);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static FieldSelector parse(String spec) {
if (spec == null || spec.isBlank()) {
return new FieldSelector(null, null);
}
Set<String> fields = new HashSet<>();
Map<String, FieldSelector> nested = new HashMap<>();
int i = 0;
while (i < spec.length()) {
int commaIdx = nextCommaOrEnd(spec, i);
int parenthesisIdx = spec.indexOf('(', i);
if (parenthesisIdx != -1 && parenthesisIdx < commaIdx) {
String name = spec.substring(i, parenthesisIdx).trim();
int closeParenthesisIdx = findMatchingParen(spec, parenthesisIdx);
String inner = spec.substring(parenthesisIdx + 1, closeParenthesisIdx);
fields.add(name);
nested.put(name, parse(inner));
i = closeParenthesisIdx + 1;
if (i < spec.length() && spec.charAt(i) == ',') i++;
} else {
String name = spec.substring(i, commaIdx).trim();
if (!name.isEmpty()) fields.add(name);
i = commaIdx + 1;
}
}
return new FieldSelector(fields, nested);
}
public static FieldSelector parse(String spec) {
return parse(spec, 0);
}
private static final int MAX_DEPTH = 32;
private static FieldSelector parse(String spec, int depth) {
if (depth >= MAX_DEPTH) {
throw new IllegalArgumentException("Field selector nesting exceeds maximum depth of " + MAX_DEPTH);
}
if (spec == null || spec.isBlank()) {
return new FieldSelector(null, null);
}
Set<String> fields = new HashSet<>();
Map<String, FieldSelector> nested = new HashMap<>();
int i = 0;
while (i < spec.length()) {
int commaIdx = nextCommaOrEnd(spec, i);
int parenthesisIdx = spec.indexOf('(', i);
if (parenthesisIdx != -1 && parenthesisIdx < commaIdx) {
String name = spec.substring(i, parenthesisIdx).trim();
int closeParenthesisIdx = findMatchingParen(spec, parenthesisIdx);
String inner = spec.substring(parenthesisIdx + 1, closeParenthesisIdx);
fields.add(name);
nested.put(name, parse(inner, depth + 1));
i = closeParenthesisIdx + 1;
if (i < spec.length() && spec.charAt(i) == ',') i++;
} else {
String name = spec.substring(i, commaIdx).trim();
if (!name.isEmpty()) fields.add(name);
i = commaIdx + 1;
}
}
return new FieldSelector(fields, nested);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@application-engine/src/main/java/com/netgrif/application/engine/serialization/FieldSelector.java`
around lines 85 - 113, `FieldSelector.parse()` is recursively parsing nested
field specs without any depth limit, so a crafted `fields` value can trigger
`StackOverflowError`. Add a recursion-depth guard to `parse()` (and its nested
`parse(inner)` call path), using a bounded counter or maximum nesting constant,
and reject overly deep specs before recursing further. Use the existing `parse`,
`findMatchingParen`, and `nextCommaOrEnd` flow to keep the change localized.

Comment on lines +1 to +36
package com.netgrif.application.engine.serialization;

import lombok.Getter;
import lombok.Setter;
import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.RequestScope;

/**
* Request-scoped holder component for managing {@link FieldSelector} instances during HTTP request processing.
* <p>
* This component is used in conjunction with {@link FieldSelectorInterceptor} to parse and store field selection
* criteria from HTTP request parameters (typically the "fields" query parameter). The held {@link FieldSelector}
* is then used by custom serializers like {@link LocalizedEventOutcomeSerializer} to control which fields
* are included in JSON response bodies.
* </p>
* <p>
* Being request-scoped ensures that each HTTP request has its own isolated instance of this holder,
* preventing thread-safety issues in concurrent request processing.
* </p>
*
* @see FieldSelector
* @see FieldSelectorInterceptor
* @see LocalizedEventOutcomeSerializer
*/
@Setter
@Getter
@Component
@RequestScope
public class FieldSelectorHolder {

/**
* The field selector that determines which fields should be included during serialization.
* Initialized with a default selector that includes all fields (parsed from null).
*/
private FieldSelector selector = FieldSelector.parse(null);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect ObjectMapperConfiguration for module registration.
fd "ObjectMapperConfiguration.java" --exec cat -n

Repository: netgrif/application-engine

Length of output: 1277


Register the SimpleModule before returning the customizer
module is populated with LocalizedEventOutcomeSerializer but never attached to the builder. findAndAddModules() only picks up discoverable modules, so this serializer is never registered and the field-selection path stays inactive.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@application-engine/src/main/java/com/netgrif/application/engine/serialization/FieldSelectorHolder.java`
around lines 1 - 36, The field-selection serializer setup is incomplete because
the SimpleModule containing LocalizedEventOutcomeSerializer is created but never
actually registered with the Jackson builder. Update the customizer that builds
the ObjectMapper so the module is explicitly attached before returning it,
rather than relying on findAndAddModules(); use the existing module-building
logic around LocalizedEventOutcomeSerializer and ensure it is added to the
builder so FieldSelectorHolder-driven selection takes effect.

Comment on lines +18 to +22
@Override
public boolean preHandle(HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) {
String fields = request.getParameter("fields");
holder.setSelector(FieldSelector.parse(fields));
return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Malformed fields parameter causes HTTP 500 instead of 400.

FieldSelector.parse() throws IllegalArgumentException for unmatched parentheses (via findMatchingParen). This propagates unhandled from preHandle(), resulting in a 500 Internal Server Error for what is a client-side input mistake. A 400 Bad Request is the correct response.

🛡️ Proposed fix: catch parse errors and reject the request
     `@Override`
     public boolean preHandle(HttpServletRequest request, `@NonNull` HttpServletResponse response, `@NonNull` Object handler) {
         String fields = request.getParameter("fields");
-        holder.setSelector(FieldSelector.parse(fields));
+        try {
+            holder.setSelector(FieldSelector.parse(fields));
+        } catch (IllegalArgumentException e) {
+            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+            return false;
+        }
         return true;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
public boolean preHandle(HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) {
String fields = request.getParameter("fields");
holder.setSelector(FieldSelector.parse(fields));
return true;
`@Override`
public boolean preHandle(HttpServletRequest request, `@NonNull` HttpServletResponse response, `@NonNull` Object handler) {
String fields = request.getParameter("fields");
try {
holder.setSelector(FieldSelector.parse(fields));
} catch (IllegalArgumentException e) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return false;
}
return true;
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 19-19: Prevent HTTP parameter pollution
Context: request.getParameter("fields")
Note: [CWE-235] Improper Handling of Extra Parameters. Security best practice.

(http-parameter-pollution)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@application-engine/src/main/java/com/netgrif/application/engine/serialization/FieldSelectorInterceptor.java`
around lines 18 - 22, The FieldSelectorInterceptor.preHandle path lets
FieldSelector.parse() throw IllegalArgumentException for malformed fields input,
which bubbles up as a 500. Update preHandle() to catch parse failures around
holder.setSelector(FieldSelector.parse(fields)) and reject the request with a
400 Bad Request instead of allowing the exception to escape; use
FieldSelector.parse and FieldSelectorInterceptor as the key places to change.

Comment on lines +99 to +101
if (outcome instanceof LocalisedCaseEventOutcome specificOutcome && selector.includes("case") && specificOutcome.getaCase() != null) {
gen.writeName("aCase");
dynamicSerializer.serializeWithSelector(specificOutcome.getaCase(), gen, context, selector.nested("case"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Selector key case does not match emitted field name aCase.

Inclusion is checked via selector.includes("case") and selector.nested("case"), but the JSON property is written as aCase. Clients must request fields=case yet receive aCase in the response — inconsistent with every other field here (net, task, data), where the query key matches the output name. Align the selector key and output name to avoid confusing API consumers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@application-engine/src/main/java/com/netgrif/application/engine/serialization/LocalizedEventOutcomeSerializer.java`
around lines 99 - 101, The `LocalizedEventOutcomeSerializer` case branch is
using the selector key `case` while emitting the JSON field name `aCase`, which
is inconsistent with the other fields. Update the `outcome instanceof
LocalisedCaseEventOutcome` handling so the selector check, nested selector, and
written property name all use the same identifier (matching the actual output
name used elsewhere in the serializer), and keep
`dynamicSerializer.serializeWithSelector` aligned with that same symbol.

Comment on lines +17 to +19
return builder -> builder
.changeDefaultPropertyInclusion(include -> include.withValueInclusion(JsonInclude.Include.NON_NULL))
.findAndAddModules();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the configuration file and surrounding package context.
FILE="application-engine/src/main/java/com/netgrif/application/engine/serialization/ObjectMapperConfiguration.java"
wc -l "$FILE"
cat -n "$FILE"

# Find where this configuration class is used and whether it applies to a single mapper/module.
rg -n "ObjectMapperConfiguration|changeDefaultPropertyInclusion|JsonInclude.Include.NON_NULL|LocalisedEventOutcome|findAndAddModules|addModule" application-engine/src/main/java

# Map nearby serialization-related classes.
fd -a ".*Serialization.*|.*ObjectMapper.*|.*Outcome.*|.*Jackson.*" application-engine/src/main/java

Repository: netgrif/application-engine

Length of output: 17895


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect related Jackson configuration and any other customizers to determine scope.
FILES=(
  "application-engine/src/main/java/com/netgrif/application/engine/configuration/properties/JacksonConfigurationProperties.java"
  "application-engine/src/main/java/com/netgrif/application/engine/serialization/ObjectMapperConfiguration.java"
  "application-engine/src/main/java/com/netgrif/application/engine/workflow/service/MenuImportExportService.java"
  "application-engine/src/main/java/com/netgrif/application/engine/workflow/service/FilterImportExportService.java"
  "application-engine/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java"
)
for f in "${FILES[@]}"; do
  if [ -f "$f" ]; then
    echo "===== $f ====="
    wc -l "$f"
    cat -n "$f" | sed -n '1,220p'
  fi
done

# Search for Jackson builder customizers and object mapper customization patterns.
rg -n "JsonMapperBuilderCustomizer|changeDefaultPropertyInclusion|findAndAddModules|addModule\\(|ObjectMapper|Jackson2ObjectMapperBuilder|JsonInclude.Include" application-engine/src/main/java

Repository: netgrif/application-engine

Length of output: 44435


Scope this inclusion change Global NON_NULL here applies to every response, while the SimpleModule is never registered, so LocalizedEventOutcomeSerializer is not used. Keep the null-handling on LocalisedEventOutcome only, or add the module separately and leave the default mapper behavior unchanged.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@application-engine/src/main/java/com/netgrif/application/engine/serialization/ObjectMapperConfiguration.java`
around lines 17 - 19, The ObjectMapperConfiguration change is too broad because
`changeDefaultPropertyInclusion` makes `NON_NULL` apply globally instead of only
to `LocalisedEventOutcome`. Update `ObjectMapperConfiguration` so the
null-handling is scoped to `LocalisedEventOutcome` (or register the
`SimpleModule` separately for `LocalizedEventOutcomeSerializer`) and keep the
mapper’s default inclusion behavior unchanged for all other responses.

@sonarqubecloud

sonarqubecloud Bot commented Jul 9, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
40.8% Coverage on New Code (required ≥ 60%)

See analysis details on SonarQube Cloud

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

Labels

improvement A change that improves on an existing feature Large

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant