Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions workflow/portable.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,13 @@ func (v PortableValue) MarshalJSON() ([]byte, error) {
if v.any == nil {
return nil, errors.New("cannot marshal zero PortableValue")
}
// Preserve the original wire bytes for a delayed value that was never read.
// Any() would generically decode the raw JSON (e.g. into map[string]any /
// float64) and lose type/large-integer fidelity on re-marshal, so short-circuit
// here to re-emit the retained JSON verbatim, matching .NET's JsonElement.
if raw, ok := v.any.(json.RawMessage); ok && v.cache == nil {
return json.Marshal(portableValueJSON{TypeID: v.TypeID, Value: raw})
}
value := v.Any()
if raw, ok := value.(json.RawMessage); ok {
return json.Marshal(portableValueJSON{TypeID: v.TypeID, Value: raw})
Expand Down
32 changes: 32 additions & 0 deletions workflow/portable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,38 @@ func delayedPortableValue(t *testing.T, pv workflow.PortableValue) workflow.Port
return delayed
}

func TestPortableValueMarshal_PreservesUntouchedDelayedRawJSON(t *testing.T) {
// A delayed value with an unregistered TypeID and a large integer that
// cannot be represented exactly by float64. Re-marshaling without any typed
// access must re-emit the original bytes verbatim, so a durable-JSON
// restore -> re-checkpoint cycle stays idempotent and loses no fidelity.
const bigInt = "9007219254740993" // > 2^53
wire := []byte(`{"TypeID":{"PackageName":"example.invalid/missing","TypeName":"Missing"},"Value":` + bigInt + `}`)

var delayed workflow.PortableValue
if err := json.Unmarshal(wire, &delayed); err != nil {
t.Fatalf("Unmarshal: %v", err)
}
if !delayed.Delayed() {
t.Fatalf("expected delayed value after unmarshal")
}

// Re-marshal WITHOUT any typed access.
data, err := json.Marshal(delayed)
if err != nil {
t.Fatalf("Marshal: %v", err)
}
var wrapper struct {
Value json.RawMessage
}
if err := json.Unmarshal(data, &wrapper); err != nil {
t.Fatalf("Unmarshal wrapper: %v", err)
}
if got := string(wrapper.Value); got != bigInt {
t.Fatalf("re-emitted Value = %s, want %s (large-integer fidelity lost)", got, bigInt)
}
}

func TestPortableValue_RejectsNil(t *testing.T) {
defer func() {
if recover() == nil {
Expand Down
Loading