Skip to content

fix: keep JS parameters serializable via custom serialization - #25275

Draft
Artur- wants to merge 1 commit into
mainfrom
jsfunction-serializable-captures
Draft

fix: keep JS parameters serializable via custom serialization#25275
Artur- wants to merge 1 commit into
mainfrom
jsfunction-serializable-captures

Conversation

@Artur-

@Artur- Artur- commented Aug 21, 2026

Copy link
Copy Markdown
Member

JsFunction and JavaScriptInvocation both declare Serializable but hold their values in a List, so nothing stopped a value that Java serialization cannot write from ending up in the HTTP session. Neither is reliably transient: addJsInitializer retains a JsFunction for the lifetime of the registration so the initializer can be re-run on every re-attach, and an invocation waits in the session when its target element is not yet attached or is invisible. A bad value surfaced as a NotSerializableException during session passivation.

Two unrelated kinds of serialization are involved. Jackson encodes the values for the browser and works for any bean; Java serialization writes them to the session and works only for Serializable. Requiring Serializable would have narrowed what executeJs accepts, undoing the bean support added in #22378 and rejecting JsonNode-typed variables, since JsonNode does not itself declare Serializable.

Instead both types now write a serialization proxy that converts each value through JacksonCodec.serializableParameter: values that are already Serializable pass through untouched, and anything else is stored as the JSON it encodes to. encodeWithoutTypeInfo passes a JsonNode through unchanged, so the client receives an identical message.

Only non-serializable values are converted, which is what keeps Element and ReturnChannelRegistration encoding deferred to the moment the UIDL message is built — an element attached only after the session is restored must still encode as a DOM reference rather than the null it would have produced earlier. Restoring an invocation skips the constructor's dry-run encode, since deserialization can reach it before the state nodes behind element parameters are fully restored.

JsFunction and JavaScriptInvocation both declare Serializable but hold
their values in a List<Object>, so nothing stopped a value that Java
serialization cannot write from ending up in the HTTP session. Neither
is reliably transient: addJsInitializer retains a JsFunction for the
lifetime of the registration so the initializer can be re-run on every
re-attach, and an invocation waits in the session when its target
element is not yet attached or is invisible. A bad value surfaced as a
NotSerializableException during session passivation.

Two unrelated kinds of serialization are involved. Jackson encodes the
values for the browser and works for any bean; Java serialization writes
them to the session and works only for Serializable. Requiring
Serializable would have narrowed what executeJs accepts, undoing the
bean support added in #22378 and rejecting JsonNode-typed variables,
since JsonNode does not itself declare Serializable.

Instead both types now write a serialization proxy that converts each
value through JacksonCodec.serializableParameter: values that are
already Serializable pass through untouched, and anything else is stored
as the JSON it encodes to. encodeWithoutTypeInfo passes a JsonNode
through unchanged, so the client receives an identical message.

Only non-serializable values are converted, which is what keeps Element
and ReturnChannelRegistration encoding deferred to the moment the UIDL
message is built — an element attached only after the session is
restored must still encode as a DOM reference rather than the null it
would have produced earlier. Restoring an invocation skips the
constructor's dry-run encode, since deserialization can reach it before
the state nodes behind element parameters are fully restored.
@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

 1 374 files  ±0   1 375 suites  ±0   1h 27m 9s ⏱️ - 5m 27s
10 473 tests +3  10 406 ✅ +3  67 💤 ±0  0 ❌ ±0 
10 792 runs  +3  10 724 ✅ +3  68 💤 ±0  0 ❌ ±0 

Results for commit e47d38c. ± Comparison against base commit 2bc163e.

@Legioth

Legioth commented Aug 21, 2026

Copy link
Copy Markdown
Member

The approach here seems broken since the decision whether to use Jackson or not is only used on the top level. It will still use Jackson for nested Element references (e.g. if you pass a Map<String, Element> to executeJs).

My gut feeling is that we might not need this functionality at all since the application is typically in control of the actual types that are used and a serializable application won't use leaf types that are not serializable. There might be be some cases of add-ons using simple records in their JSON format but that's no different that add-ons that might use simple records for storing "tuples" in instance fields.

So I'd suggest that we instead just document that it's recommended that parameters are Serializable.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants