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);
+ }
+
+ /**
+ * Checks whether the specified field should be included in serialization.
+ *
+ * A field is included if:
+ *
+ * - The selector is in unrestricted mode (fields is null), or
+ * - The field is explicitly listed in the fields set, or
+ * - The field has a nested selector defined
+ *
+ *
+ *
+ * @param field the name of the field to check
+ * @return true if the field should be included, false otherwise
+ */
+ public boolean includes(String field) {
+ if (fields == null) return true;
+ if (fields.contains(field)) return true;
+ return nested != null && nested.containsKey(field);
+ }
+
+ /**
+ * Checks whether this selector is in unrestricted mode, allowing all fields.
+ *
+ * Unrestricted mode occurs when no field specification was provided during parsing
+ * (i.e., the specification was null or blank).
+ *
+ *
+ * @return true if all fields should be included without restrictions, false otherwise
+ */
+ public boolean includeAll() {
+ return fields == null;
+ }
+
+ /**
+ * Retrieves the nested FieldSelector for the specified field.
+ *
+ * If no nested selector is defined for the field, returns an unrestricted selector
+ * that allows all fields at the nested level.
+ *
+ *
+ * @param field the name of the field whose nested selector to retrieve
+ * @return the nested FieldSelector for the field, or an unrestricted selector if none exists
+ */
+ public FieldSelector nested(String field) {
+ if (nested == null || !nested.containsKey(field)) {
+ return new FieldSelector(null, null); // no restriction on nested level either
+ }
+ return nested.get(field);
+ }
+
+ /**
+ * Finds the index of the next comma at the current nesting depth, or the end of the string.
+ *
+ * This method tracks parenthesis depth to avoid splitting on commas that are inside
+ * nested field specifications. Only commas at depth 0 (not inside any parentheses)
+ * are considered as field separators.
+ *
+ *
+ * @param spec the specification string to search
+ * @param from the starting index for the search
+ * @return the index of the next comma at depth 0, or the length of the string if none found
+ */
+ private static int nextCommaOrEnd(String spec, int from) {
+ int depth = 0;
+ for (int i = from; i < spec.length(); i++) {
+ char c = spec.charAt(i);
+ if (c == '(') depth++;
+ else if (c == ')') depth--;
+ else if (c == ',' && depth == 0) return i;
+ }
+ return spec.length();
+ }
+
+ /**
+ * Finds the index of the closing parenthesis that matches the opening parenthesis at the specified position.
+ *
+ * This method correctly handles nested parentheses by tracking the depth level and returning
+ * the index of the closing parenthesis that brings the depth back to 0.
+ *
+ *
+ * @param spec the specification string to search
+ * @param openIdx the index of the opening parenthesis
+ * @return the index of the matching closing parenthesis
+ * @throws IllegalArgumentException if no matching closing parenthesis is found
+ */
+ private static int findMatchingParen(String spec, int openIdx) {
+ int depth = 0;
+ for (int i = openIdx; i < spec.length(); i++) {
+ char c = spec.charAt(i);
+ if (c == '(') depth++;
+ else if (c == ')') {
+ depth--;
+ if (depth == 0) return i;
+ }
+ }
+ throw new IllegalArgumentException("Unmatched '(' in fields selector: " + spec);
+ }
+}
diff --git a/application-engine/src/main/java/com/netgrif/application/engine/serialization/FieldSelectorHolder.java b/application-engine/src/main/java/com/netgrif/application/engine/serialization/FieldSelectorHolder.java
new file mode 100644
index 00000000000..fd21df71241
--- /dev/null
+++ b/application-engine/src/main/java/com/netgrif/application/engine/serialization/FieldSelectorHolder.java
@@ -0,0 +1,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.
+ *
+ * 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.
+ *
+ *
+ * Being request-scoped ensures that each HTTP request has its own isolated instance of this holder,
+ * preventing thread-safety issues in concurrent request processing.
+ *
+ *
+ * @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);
+}
diff --git a/application-engine/src/main/java/com/netgrif/application/engine/serialization/FieldSelectorInterceptor.java b/application-engine/src/main/java/com/netgrif/application/engine/serialization/FieldSelectorInterceptor.java
new file mode 100644
index 00000000000..e3e53292f2d
--- /dev/null
+++ b/application-engine/src/main/java/com/netgrif/application/engine/serialization/FieldSelectorInterceptor.java
@@ -0,0 +1,24 @@
+package com.netgrif.application.engine.serialization;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.lang.NonNull;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+@Component
+public class FieldSelectorInterceptor implements HandlerInterceptor {
+
+ private final FieldSelectorHolder holder;
+
+ public FieldSelectorInterceptor(FieldSelectorHolder holder) {
+ this.holder = holder;
+ }
+
+ @Override
+ public boolean preHandle(HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) {
+ String fields = request.getParameter("fields");
+ holder.setSelector(FieldSelector.parse(fields));
+ return true;
+ }
+}
diff --git a/application-engine/src/main/java/com/netgrif/application/engine/serialization/LocalizedEventOutcomeSerializer.java b/application-engine/src/main/java/com/netgrif/application/engine/serialization/LocalizedEventOutcomeSerializer.java
new file mode 100644
index 00000000000..4f544a24c63
--- /dev/null
+++ b/application-engine/src/main/java/com/netgrif/application/engine/serialization/LocalizedEventOutcomeSerializer.java
@@ -0,0 +1,122 @@
+package com.netgrif.application.engine.serialization;
+
+import com.netgrif.application.engine.workflow.web.responsebodies.eventoutcomes.LocalisedGetDataEventOutcome;
+import com.netgrif.application.engine.workflow.web.responsebodies.eventoutcomes.LocalisedGetDataGroupsEventOutcome;
+import com.netgrif.application.engine.workflow.web.responsebodies.eventoutcomes.LocalisedSetDataEventOutcome;
+import com.netgrif.application.engine.workflow.web.responsebodies.eventoutcomes.base.LocalisedCaseEventOutcome;
+import com.netgrif.application.engine.workflow.web.responsebodies.eventoutcomes.base.LocalisedEventOutcome;
+import com.netgrif.application.engine.workflow.web.responsebodies.eventoutcomes.base.LocalisedPetriNetEventOutcome;
+import com.netgrif.application.engine.workflow.web.responsebodies.eventoutcomes.base.LocalisedTaskEventOutcome;
+import org.springframework.boot.jackson.JacksonComponent;
+import tools.jackson.core.JsonGenerator;
+import tools.jackson.databind.SerializationContext;
+import tools.jackson.databind.ValueSerializer;
+
+import java.io.IOException;
+
+/**
+ * Custom JSON serializer for {@link LocalisedEventOutcome} objects that provides selective field serialization
+ * based on a {@link FieldSelector} configuration.
+ *
+ * This serializer handles various types of localized event outcomes including:
+ *
+ * - {@link LocalisedPetriNetEventOutcome} - outcomes related to Petri nets
+ * - {@link LocalisedCaseEventOutcome} - outcomes related to cases
+ * - {@link LocalisedTaskEventOutcome} - outcomes related to tasks
+ * - {@link LocalisedGetDataEventOutcome} - outcomes for data retrieval operations
+ * - {@link LocalisedGetDataGroupsEventOutcome} - outcomes for data groups retrieval
+ * - {@link LocalisedSetDataEventOutcome} - outcomes for data modification operations
+ *
+ * The serializer uses a {@link DynamicFieldSerializer} to handle nested object serialization with field selection support.
+ *
+ *
+ * @see LocalisedEventOutcome
+ * @see FieldSelector
+ * @see DynamicFieldSerializer
+ */
+@JacksonComponent(type = LocalisedEventOutcome.class)
+public class LocalizedEventOutcomeSerializer extends ValueSerializer {
+
+ /**
+ * Holder containing the {@link FieldSelector} that determines which fields should be included
+ * in the serialized JSON output.
+ */
+ private final FieldSelectorHolder selectorHolder;
+
+ /**
+ * Serializer used for dynamic field serialization of nested objects with field selection support.
+ */
+ private final DynamicFieldSerializer dynamicSerializer;
+
+ /**
+ * Constructs a new LocalizedEventOutcomeSerializer with the specified field selector holder.
+ *
+ * @param selectorHolder the holder containing the field selector configuration for controlling
+ * which fields are included in the serialized output
+ */
+ public LocalizedEventOutcomeSerializer(FieldSelectorHolder selectorHolder) {
+ this.selectorHolder = selectorHolder;
+ this.dynamicSerializer = new DynamicFieldSerializer(selectorHolder);
+ }
+
+ /**
+ * Serializes a {@link LocalisedEventOutcome} object to JSON format with selective field inclusion
+ * based on the configured {@link FieldSelector}.
+ *
+ * The method handles serialization of common fields (message, frontActions, outcomes) and type-specific
+ * fields based on the actual runtime type of the outcome object. Nested objects are serialized using
+ * the {@link DynamicFieldSerializer} with their corresponding nested field selectors.
+ *
+ *
+ * @param outcome the localized event outcome object to serialize
+ * @param gen the JSON generator used to write JSON content
+ * @param provider the serializer provider for accessing additional serializers
+ * @throws IOException if an I/O error occurs during serialization
+ */
+ @Override
+ public void serialize(LocalisedEventOutcome outcome, JsonGenerator gen, SerializationContext context) {
+ gen.writeStartObject();
+ FieldSelector selector = selectorHolder.getSelector();
+
+ if (selector.includes("message") && outcome.getMessage() != null) {
+ gen.writePOJOProperty("message", outcome.getMessage());
+ }
+ if (selector.includes("frontActions") && outcome.getFrontActions() != null) {
+ gen.writePOJOProperty("frontActions", outcome.getFrontActions());
+ }
+ if (selector.includes("outcomes")
+ && outcome.getOutcomes() != null) {
+ gen.writeArrayPropertyStart("outcomes");
+ for (LocalisedEventOutcome child : outcome.getOutcomes()) {
+ context.findValueSerializer(child.getClass()).serialize(child, gen, context);
+ }
+ gen.writeEndArray();
+ }
+ if (outcome instanceof LocalisedPetriNetEventOutcome specificOutcome && selector.includes("net") && specificOutcome.getNet() != null) {
+ gen.writeName("net");
+ dynamicSerializer.serializeWithSelector(specificOutcome.getNet(), gen, context, selector.nested("net"));
+ }
+ if (outcome instanceof LocalisedCaseEventOutcome specificOutcome && selector.includes("case") && specificOutcome.getaCase() != null) {
+ gen.writeName("aCase");
+ dynamicSerializer.serializeWithSelector(specificOutcome.getaCase(), gen, context, selector.nested("case"));
+ }
+ if (outcome instanceof LocalisedTaskEventOutcome specificOutcome && selector.includes("task") && specificOutcome.getTask() != null) {
+ gen.writeName("task");
+ dynamicSerializer.serializeWithSelector(specificOutcome.getTask(), gen, context, selector.nested("task"));
+ }
+ if (outcome instanceof LocalisedGetDataEventOutcome specificOutcome && selector.includes("data") && specificOutcome.getData() != null) {
+ gen.writeName("data");
+ dynamicSerializer.serializeWithSelector(specificOutcome.getData(), gen, context, selector.nested("data"));
+ }
+ if (outcome instanceof LocalisedGetDataGroupsEventOutcome specificOutcome && selector.includes("data") && specificOutcome.getData() != null) {
+ gen.writeName("data");
+ dynamicSerializer.serializeWithSelector(specificOutcome.getData(), gen, context, selector.nested("data"));
+ }
+ if (outcome instanceof LocalisedSetDataEventOutcome specificOutcome && selector.includes("changedFields") && specificOutcome.getChangedFields() != null) {
+ gen.writeName("changedFields");
+ dynamicSerializer.serializeWithSelector(specificOutcome.getChangedFields(), gen, context, selector.nested("changedFields"));
+ }
+
+ gen.writeEndObject();
+ }
+}
diff --git a/application-engine/src/main/java/com/netgrif/application/engine/serialization/ObjectMapperConfiguration.java b/application-engine/src/main/java/com/netgrif/application/engine/serialization/ObjectMapperConfiguration.java
new file mode 100644
index 00000000000..f311a8205c1
--- /dev/null
+++ b/application-engine/src/main/java/com/netgrif/application/engine/serialization/ObjectMapperConfiguration.java
@@ -0,0 +1,21 @@
+package com.netgrif.application.engine.serialization;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.netgrif.application.engine.workflow.web.responsebodies.eventoutcomes.base.LocalisedEventOutcome;
+import org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import tools.jackson.databind.module.SimpleModule;
+
+@Configuration
+public class ObjectMapperConfiguration {
+
+ @Bean
+ public JsonMapperBuilderCustomizer jsonCustomizer(FieldSelectorHolder holder) {
+ SimpleModule module = new SimpleModule();
+ module.addSerializer(LocalisedEventOutcome.class, new LocalizedEventOutcomeSerializer(holder));
+ return builder -> builder
+ .changeDefaultPropertyInclusion(include -> include.withValueInclusion(JsonInclude.Include.NON_NULL))
+ .findAndAddModules();
+ }
+}