elsa-workflows / elsa-workflows/elsa-core
ObjectExpressionHandler hardcodes JsonSerializerOptions (Object/Json expressions ignore DI converters)
- Dominant language
- C#
- Stars
- 7.9k
- Forks
- 1.5k
- Avg merge
- 15h 22m
- Merged PRs (30d)
- 114
Description
## Problem
`ObjectExpressionHandler` (expression types **Object** and **Json**) builds a private, instance-cached `JsonSerializerOptions` with a fixed converter set and never consults host/DI `IJsonSerializer` / `ISerializationOptionsConfigurator`. Custom `JsonConverter`s registered by the host are ignored for Object/Json expression evaluation.
This is the same bug *class* as #7194 (`VariableExtensions`) and closed #8131 (`GetInput` static cache, fixed in #8158), but a **different entry point**.
## Evidence (main)
`src/modules/Elsa.Workflows.Core/Expressions/ObjectExpressionHandler.cs`:
```csharp
private JsonSerializerOptions? _serializerOptions;
private JsonSerializerOptions SerializerOptions =>
_serializerOptions ??= new JsonSerializerOptions { /* camelCase, Preserve, … */ }
.WithConverters(
new IntegerJsonConverter(),
new DecimalJsonConverter(),
new JsonStringEnumConverter());
public ValueTask EvaluateAsync(...)
{
var converterOptions = new ObjectConverterOptions(SerializerOptions);
var model = value.ConvertTo(returnType, converterOptions);
…
}
```
Registered for both descriptors in `DefaultExpressionDescriptorProvider` (`CreateObjectDescriptor` / `CreateJsonDescriptor`).
Contrast after #8158 — `ExpressionExecutionContextExtensions.GetInput`:
```csharp
return context.GetRequiredService().GetOptions().CloneForValueConversion();
```
Storage driver already uses `payloadSerializer.GetOptions().Clone()`.
## Why it matters
Three value-conversion stacks remain inconsistent after #8158:
| Path | Options source |
|------|----------------|
| `GetInput` | Host DI (`IJsonSerializer`) ✅ |
| `WorkflowInstanceStorageDriver` | Host DI ✅ |
| `VariableExtensions.ParseValue` | Hardcoded static (#7194) |
| `ObjectExpressionHandler` | Hardcoded private cache (**this**) |
Object/Json literals with custom-typed shapes fail or mis-convert while the same type works via `GetInput` / storage.
## Proposed subtractive direction
1. Resolve options from `context.GetRequiredService().GetOptions().CloneForValueConversion()` (same helper as `GetInput`).
2. Drop the private `_serializerOptions` field; keep Integer/Decimal converters only if still needed after host options (prefer host registration over a second hardcode).
3. Share one small helper with the eventual #7194 fix so Object/Json/Variable parse are not three one-offs.
4. Test: register a custom converter via `ISerializationOptionsConfigurator`; evaluate an Object/Json expression targeting that type — must succeed.
## Not a duplicate of
- #7194 — `VariableExtensions` static options (sibling; cite together, fix separately or with shared helper)
- #8131 (closed) / PR #8158 — `GetInput` only
- #8046 — unknown expression type fail-open on definition deserialize
- #6745 — `ObjectFormatter` / VariableMapper round-trip
- #7770 — activity *output* converter feature (new capability, not this path)
## Milestone
Unset — Triage / Crew Lead place. Do not auto-assign Engineer. Do not add Ready.
Contributor guide
Research direction
Start with src/modules/Elsa.Workflows.Core/Expressions/ObjectExpressionHandler.cs and compare its cached SerializerOptions with ExpressionExecutionContextExtensions.GetInput and the storage driver’s options handling. Trace CreateObjectDescriptor and CreateJsonDescriptor, then add coverage that configures a custom converter through ISerializationOptionsConfigurator and evaluates an Object or Json expression; done means the converter is honored.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 70/100