Skip to content

Conversation

@devin-ai-integration
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot commented Oct 14, 2025

Closes #4209

Description

This PR improves JSON decode error messages by adding path context showing where in the JSON structure errors occur.

Before:

missing property: type

After:

missing property: type (at array[0].field:myField)

Implementation

Added a path tracking system using a pathElement interface to avoid string allocation during successful decoding:

  • Path tracking: pathContext []pathElement field on Decoder with pushPath()/popPath() methods
  • Path element types: indexPathElement (array indices), fieldPathElement (struct fields), staticPathElement (static strings like "type")
  • Lazy evaluation: Path string only built via strings.Builder when error occurs
  • Consistent usage: Created *WithContext versions of all jsonObject helper methods and migrated decode methods to use them

Files to Review

Critical:

  • encoding/json/decode.go:38-66: Path element interface and implementations
  • encoding/json/decode.go:195-217: Path management methods
  • encoding/json/decode.go:1536-1558: GetWithContext error handling
  • encoding/json/decode.go:850-861: Example of path tracking in decodeCompositeField

Testing:

  • encoding/json/encoding_test.go:4027-4142: Comprehensive error context tests

Known Issues

⚠️ Incomplete migration: The decodeAuthorization method still uses old Get() on nested entitlement objects (lines 1150, 1156, 1164). These specific errors won't include path context. Reviewer should verify if this is acceptable or needs fixing.

Performance Considerations

Path tracking adds overhead to every decode operation (pushPath/popPath calls), but string building only occurs on error. Pre-allocated slice capacity of 8 minimizes reallocations for typical JSON depths.


Link to Devin run: https://app.devin.ai/sessions/5e6d0a0c3c03459d8c6aaa8d5feae034
Requested by: [email protected]

This commit improves error messages when JSON decoding fails by adding
context about where in the JSON structure the error occurred.

Changes:
- Added pathContext field to Decoder to track the current location
- Added path management helper methods (pushPath, popPath, getPathString)
- Added *WithContext versions of jsonObject methods that use path context
- Updated key decode methods to track and use path context

For example, instead of:
  missing property: type

Users now see:
  missing property: type (at array[0])
  missing property: type (at field:myField.type)

Closes #4209
@devin-ai-integration
Copy link
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@github-actions
Copy link

github-actions bot commented Oct 14, 2025

Benchstat comparison

  • Base branch: onflow:master
  • Base commit: 3ec0c24
Results

old.txtnew.txt
time/opdelta
pkg:github.com/onflow/cadence/runtime goos:linux goarch:amd64
ContractFunctionInvocation-4414µs ± 0%381µs ± 0%~(p=1.000 n=1+1)
pkg:github.com/onflow/cadence/encoding/ccf goos:linux goarch:amd64
DecodeBatchEventsCCF-4136ms ± 0%139ms ± 0%~(p=1.000 n=1+1)
DecodeBatchEventsJSON-4389ms ± 0%392ms ± 0%~(p=1.000 n=1+1)
DecodeCCF/FlowFees.FeesDeducted-42.97µs ± 0%2.91µs ± 0%~(p=1.000 n=1+1)
DecodeCCF/FlowFees.TokensWithdrawn-42.23µs ± 0%2.16µs ± 0%~(p=1.000 n=1+1)
DecodeCCF/FlowIDTableStaking.DelegatorRewardsPaid-42.88µs ± 0%2.82µs ± 0%~(p=1.000 n=1+1)
DecodeCCF/FlowIDTableStaking.EpochTotalRewardsPaid-43.24µs ± 0%3.19µs ± 0%~(p=1.000 n=1+1)
DecodeCCF/FlowIDTableStaking.NewWeeklyPayout-42.19µs ± 0%2.17µs ± 0%~(p=1.000 n=1+1)
DecodeCCF/FlowIDTableStaking.RewardsPaid-42.57µs ± 0%2.52µs ± 0%~(p=1.000 n=1+1)
DecodeCCF/FlowToken.TokensDeposited-42.66µs ± 0%2.61µs ± 0%~(p=1.000 n=1+1)
DecodeCCF/FlowToken.TokensDeposited_with_nil_receiver-42.58µs ± 0%2.53µs ± 0%~(p=1.000 n=1+1)
DecodeCCF/FlowToken.TokensMinted-42.21µs ± 0%2.17µs ± 0%~(p=1.000 n=1+1)
DecodeCCF/FlowToken.TokensWithdrawn-42.70µs ± 0%2.62µs ± 0%~(p=1.000 n=1+1)
DecodeJSON/FlowFees.FeesDeducted-49.94µs ± 0%9.79µs ± 0%~(p=1.000 n=1+1)
DecodeJSON/FlowFees.TokensWithdrawn-45.70µs ± 0%5.59µs ± 0%~(p=1.000 n=1+1)
DecodeJSON/FlowIDTableStaking.DelegatorRewardsPaid-48.93µs ± 0%8.78µs ± 0%~(p=1.000 n=1+1)
DecodeJSON/FlowIDTableStaking.EpochTotalRewardsPaid-412.3µs ± 0%12.0µs ± 0%~(p=1.000 n=1+1)
DecodeJSON/FlowIDTableStaking.NewWeeklyPayout-45.77µs ± 0%5.72µs ± 0%~(p=1.000 n=1+1)
DecodeJSON/FlowIDTableStaking.RewardsPaid-47.37µs ± 0%7.42µs ± 0%~(p=1.000 n=1+1)
DecodeJSON/FlowToken.TokensDeposited-47.68µs ± 0%7.79µs ± 0%~(p=1.000 n=1+1)
DecodeJSON/FlowToken.TokensDeposited_with_nil_receiver-47.12µs ± 0%7.06µs ± 0%~(p=1.000 n=1+1)
DecodeJSON/FlowToken.TokensMinted-45.60µs ± 0%5.65µs ± 0%~(p=1.000 n=1+1)
DecodeJSON/FlowToken.TokensWithdrawn-47.65µs ± 0%7.77µs ± 0%~(p=1.000 n=1+1)
EncodeBatchEventsCCF-487.6ms ± 0%86.6ms ± 0%~(p=1.000 n=1+1)
EncodeBatchEventsJSON-4100ms ± 0%97ms ± 0%~(p=1.000 n=1+1)
EncodeCCF/FlowFees.FeesDeducted-41.91µs ± 0%1.92µs ± 0%~(p=1.000 n=1+1)
EncodeCCF/FlowFees.TokensWithdrawn-41.63µs ± 0%1.54µs ± 0%~(p=1.000 n=1+1)
EncodeCCF/FlowIDTableStaking.DelegatorRewardsPaid-41.88µs ± 0%1.89µs ± 0%~(p=1.000 n=1+1)
EncodeCCF/FlowIDTableStaking.EpochTotalRewardsPaid-42.15µs ± 0%2.17µs ± 0%~(p=1.000 n=1+1)
EncodeCCF/FlowIDTableStaking.NewWeeklyPayout-41.60µs ± 0%1.60µs ± 0%~(p=1.000 n=1+1)
EncodeCCF/FlowIDTableStaking.RewardsPaid-41.73µs ± 0%1.73µs ± 0%~(p=1.000 n=1+1)
EncodeCCF/FlowToken.TokensDeposited-41.76µs ± 0%1.75µs ± 0%~(p=1.000 n=1+1)
EncodeCCF/FlowToken.TokensDeposited_with_nil_receiver-41.74µs ± 0%1.73µs ± 0%~(p=1.000 n=1+1)
EncodeCCF/FlowToken.TokensMinted-41.56µs ± 0%1.54µs ± 0%~(p=1.000 n=1+1)
EncodeCCF/FlowToken.TokensWithdrawn-41.79µs ± 0%1.77µs ± 0%~(p=1.000 n=1+1)
EncodeJSON/FlowFees.FeesDeducted-42.27µs ± 0%2.24µs ± 0%~(p=1.000 n=1+1)
EncodeJSON/FlowFees.TokensWithdrawn-41.34µs ± 0%1.31µs ± 0%~(p=1.000 n=1+1)
EncodeJSON/FlowIDTableStaking.DelegatorRewardsPaid-42.09µs ± 0%2.05µs ± 0%~(p=1.000 n=1+1)
EncodeJSON/FlowIDTableStaking.EpochTotalRewardsPaid-42.80µs ± 0%2.76µs ± 0%~(p=1.000 n=1+1)
EncodeJSON/FlowIDTableStaking.NewWeeklyPayout-41.36µs ± 0%1.35µs ± 0%~(p=1.000 n=1+1)
EncodeJSON/FlowIDTableStaking.RewardsPaid-41.76µs ± 0%1.73µs ± 0%~(p=1.000 n=1+1)
EncodeJSON/FlowToken.TokensDeposited-42.08µs ± 0%2.00µs ± 0%~(p=1.000 n=1+1)
EncodeJSON/FlowToken.TokensDeposited_with_nil_receiver-41.60µs ± 0%1.58µs ± 0%~(p=1.000 n=1+1)
EncodeJSON/FlowToken.TokensMinted-41.35µs ± 0%1.32µs ± 0%~(p=1.000 n=1+1)
EncodeJSON/FlowToken.TokensWithdrawn-42.02µs ± 0%1.97µs ± 0%~(p=1.000 n=1+1)
pkg:github.com/onflow/cadence/runtime goos:linux goarch:amd64
ExportType/composite_type-4263ns ± 0%266ns ± 0%~(p=1.000 n=1+1)
ExportType/simple_type-478.0ns ± 0%78.2ns ± 0%~(p=1.000 n=1+1)
pkg:github.com/onflow/cadence/interpreter goos:linux goarch:amd64
ImperativeFib-420.2µs ± 0%20.1µs ± 0%~(p=1.000 n=1+1)
InterpretRecursionFib-42.09ms ± 0%2.17ms ± 0%~(p=1.000 n=1+1)
NewInterpreter/new_interpreter-41.04µs ± 0%1.04µs ± 0%~(p=1.000 n=1+1)
NewInterpreter/new_sub-interpreter-4353ns ± 0%324ns ± 0%~(p=1.000 n=1+1)
pkg:github.com/onflow/cadence/runtime goos:linux goarch:amd64
RuntimeFungibleTokenTransferInterpreter-4606µs ± 0%572µs ± 0%~(p=1.000 n=1+1)
RuntimeFungibleTokenTransferVM-4667µs ± 0%623µs ± 0%~(p=1.000 n=1+1)
RuntimeResourceDictionaryValues-42.77ms ± 0%2.58ms ± 0%~(p=1.000 n=1+1)
RuntimeResourceTracking-413.2ms ± 0%11.6ms ± 0%~(p=1.000 n=1+1)
RuntimeScriptNoop-415.5µs ± 0%14.6µs ± 0%~(p=1.000 n=1+1)
RuntimeVMInvokeContractImperativeFib-428.7µs ± 0%28.6µs ± 0%~(p=1.000 n=1+1)
pkg:github.com/onflow/cadence/interpreter goos:linux goarch:amd64
ValueIsSubtypeOfSemaType-467.5ns ± 0%67.0ns ± 0%~(p=1.000 n=1+1)
 
alloc/opdelta
pkg:github.com/onflow/cadence/runtime goos:linux goarch:amd64
ContractFunctionInvocation-4152kB ± 0%152kB ± 0%~(p=1.000 n=1+1)
pkg:github.com/onflow/cadence/encoding/ccf goos:linux goarch:amd64
DecodeBatchEventsCCF-471.5MB ± 0%71.5MB ± 0%~(p=1.000 n=1+1)
DecodeBatchEventsJSON-4243MB ± 0%252MB ± 0%~(p=1.000 n=1+1)
DecodeCCF/FlowFees.FeesDeducted-41.50kB ± 0%1.50kB ± 0%~(all equal)
DecodeCCF/FlowFees.TokensWithdrawn-41.31kB ± 0%1.31kB ± 0%~(all equal)
DecodeCCF/FlowIDTableStaking.DelegatorRewardsPaid-41.58kB ± 0%1.58kB ± 0%~(all equal)
DecodeCCF/FlowIDTableStaking.EpochTotalRewardsPaid-41.60kB ± 0%1.60kB ± 0%~(all equal)
DecodeCCF/FlowIDTableStaking.NewWeeklyPayout-41.36kB ± 0%1.36kB ± 0%~(all equal)
DecodeCCF/FlowIDTableStaking.RewardsPaid-41.48kB ± 0%1.48kB ± 0%~(all equal)
DecodeCCF/FlowToken.TokensDeposited-41.42kB ± 0%1.42kB ± 0%~(all equal)
DecodeCCF/FlowToken.TokensDeposited_with_nil_receiver-41.41kB ± 0%1.41kB ± 0%~(all equal)
DecodeCCF/FlowToken.TokensMinted-41.31kB ± 0%1.31kB ± 0%~(all equal)
DecodeCCF/FlowToken.TokensWithdrawn-41.43kB ± 0%1.43kB ± 0%~(all equal)
DecodeJSON/FlowFees.FeesDeducted-45.95kB ± 0%6.16kB ± 0%~(p=1.000 n=1+1)
DecodeJSON/FlowFees.TokensWithdrawn-43.55kB ± 0%3.73kB ± 0%~(p=1.000 n=1+1)
DecodeJSON/FlowIDTableStaking.DelegatorRewardsPaid-45.40kB ± 0%5.61kB ± 0%~(p=1.000 n=1+1)
DecodeJSON/FlowIDTableStaking.EpochTotalRewardsPaid-47.32kB ± 0%7.55kB ± 0%~(p=1.000 n=1+1)
DecodeJSON/FlowIDTableStaking.NewWeeklyPayout-43.61kB ± 0%3.79kB ± 0%~(p=1.000 n=1+1)
DecodeJSON/FlowIDTableStaking.RewardsPaid-44.50kB ± 0%4.70kB ± 0%~(p=1.000 n=1+1)
DecodeJSON/FlowToken.TokensDeposited-44.85kB ± 0%5.04kB ± 0%~(p=1.000 n=1+1)
DecodeJSON/FlowToken.TokensDeposited_with_nil_receiver-44.42kB ± 0%4.62kB ± 0%~(p=1.000 n=1+1)
DecodeJSON/FlowToken.TokensMinted-43.57kB ± 0%3.75kB ± 0%~(p=1.000 n=1+1)
DecodeJSON/FlowToken.TokensWithdrawn-44.85kB ± 0%5.04kB ± 0%~(p=1.000 n=1+1)
EncodeBatchEventsCCF-465.8MB ± 0%65.8MB ± 0%~(p=1.000 n=1+1)
EncodeBatchEventsJSON-440.7MB ± 0%40.7MB ± 0%~(p=1.000 n=1+1)
EncodeCCF/FlowFees.FeesDeducted-41.29kB ± 0%1.29kB ± 0%~(all equal)
EncodeCCF/FlowFees.TokensWithdrawn-41.24kB ± 0%1.24kB ± 0%~(all equal)
EncodeCCF/FlowIDTableStaking.DelegatorRewardsPaid-41.51kB ± 0%1.51kB ± 0%~(all equal)
EncodeCCF/FlowIDTableStaking.EpochTotalRewardsPaid-41.48kB ± 0%1.48kB ± 0%~(all equal)
EncodeCCF/FlowIDTableStaking.NewWeeklyPayout-41.42kB ± 0%1.42kB ± 0%~(all equal)
EncodeCCF/FlowIDTableStaking.RewardsPaid-41.50kB ± 0%1.50kB ± 0%~(all equal)
EncodeCCF/FlowToken.TokensDeposited-41.29kB ± 0%1.29kB ± 0%~(all equal)
EncodeCCF/FlowToken.TokensDeposited_with_nil_receiver-41.27kB ± 0%1.27kB ± 0%~(all equal)
EncodeCCF/FlowToken.TokensMinted-41.24kB ± 0%1.24kB ± 0%~(all equal)
EncodeCCF/FlowToken.TokensWithdrawn-41.29kB ± 0%1.29kB ± 0%~(all equal)
EncodeJSON/FlowFees.FeesDeducted-4896B ± 0%896B ± 0%~(all equal)
EncodeJSON/FlowFees.TokensWithdrawn-4536B ± 0%536B ± 0%~(all equal)
EncodeJSON/FlowIDTableStaking.DelegatorRewardsPaid-4920B ± 0%920B ± 0%~(all equal)
EncodeJSON/FlowIDTableStaking.EpochTotalRewardsPaid-41.11kB ± 0%1.11kB ± 0%~(all equal)
EncodeJSON/FlowIDTableStaking.NewWeeklyPayout-4584B ± 0%584B ± 0%~(all equal)
EncodeJSON/FlowIDTableStaking.RewardsPaid-4784B ± 0%784B ± 0%~(all equal)
EncodeJSON/FlowToken.TokensDeposited-4808B ± 0%808B ± 0%~(all equal)
EncodeJSON/FlowToken.TokensDeposited_with_nil_receiver-4672B ± 0%672B ± 0%~(all equal)
EncodeJSON/FlowToken.TokensMinted-4544B ± 0%544B ± 0%~(all equal)
EncodeJSON/FlowToken.TokensWithdrawn-4800B ± 0%800B ± 0%~(all equal)
pkg:github.com/onflow/cadence/runtime goos:linux goarch:amd64
ExportType/composite_type-4120B ± 0%120B ± 0%~(all equal)
ExportType/simple_type-40.00B 0.00B ~(all equal)
pkg:github.com/onflow/cadence/interpreter goos:linux goarch:amd64
ImperativeFib-48.30kB ± 0%8.30kB ± 0%~(all equal)
InterpretRecursionFib-41.19MB ± 0%1.19MB ± 0%~(all equal)
NewInterpreter/new_interpreter-4976B ± 0%976B ± 0%~(all equal)
NewInterpreter/new_sub-interpreter-4232B ± 0%232B ± 0%~(all equal)
pkg:github.com/onflow/cadence/runtime goos:linux goarch:amd64
RuntimeFungibleTokenTransferInterpreter-4155kB ± 0%156kB ± 0%~(p=1.000 n=1+1)
RuntimeFungibleTokenTransferVM-4173kB ± 0%174kB ± 0%~(p=1.000 n=1+1)
RuntimeResourceDictionaryValues-41.77MB ± 0%1.76MB ± 0%~(p=1.000 n=1+1)
RuntimeResourceTracking-49.26MB ± 0%9.27MB ± 0%~(p=1.000 n=1+1)
RuntimeScriptNoop-47.99kB ± 0%7.98kB ± 0%~(p=1.000 n=1+1)
RuntimeVMInvokeContractImperativeFib-410.3kB ± 0%10.3kB ± 0%~(all equal)
pkg:github.com/onflow/cadence/interpreter goos:linux goarch:amd64
ValueIsSubtypeOfSemaType-448.0B ± 0%48.0B ± 0%~(all equal)
 
allocs/opdelta
pkg:github.com/onflow/cadence/runtime goos:linux goarch:amd64
ContractFunctionInvocation-42.47k ± 0%2.48k ± 0%~(p=1.000 n=1+1)
pkg:github.com/onflow/cadence/encoding/ccf goos:linux goarch:amd64
DecodeBatchEventsCCF-41.53M ± 0%1.53M ± 0%~(p=1.000 n=1+1)
DecodeBatchEventsJSON-44.68M ± 0%4.84M ± 0%~(p=1.000 n=1+1)
DecodeCCF/FlowFees.FeesDeducted-431.0 ± 0%31.0 ± 0%~(all equal)
DecodeCCF/FlowFees.TokensWithdrawn-427.0 ± 0%27.0 ± 0%~(all equal)
DecodeCCF/FlowIDTableStaking.DelegatorRewardsPaid-431.0 ± 0%31.0 ± 0%~(all equal)
DecodeCCF/FlowIDTableStaking.EpochTotalRewardsPaid-433.0 ± 0%33.0 ± 0%~(all equal)
DecodeCCF/FlowIDTableStaking.NewWeeklyPayout-427.0 ± 0%27.0 ± 0%~(all equal)
DecodeCCF/FlowIDTableStaking.RewardsPaid-430.0 ± 0%30.0 ± 0%~(all equal)
DecodeCCF/FlowToken.TokensDeposited-432.0 ± 0%32.0 ± 0%~(all equal)
DecodeCCF/FlowToken.TokensDeposited_with_nil_receiver-430.0 ± 0%30.0 ± 0%~(all equal)
DecodeCCF/FlowToken.TokensMinted-427.0 ± 0%27.0 ± 0%~(all equal)
DecodeCCF/FlowToken.TokensWithdrawn-432.0 ± 0%32.0 ± 0%~(all equal)
DecodeJSON/FlowFees.FeesDeducted-4125 ± 0%129 ± 0%~(p=1.000 n=1+1)
DecodeJSON/FlowFees.TokensWithdrawn-470.0 ± 0%72.0 ± 0%~(p=1.000 n=1+1)
DecodeJSON/FlowIDTableStaking.DelegatorRewardsPaid-4101 ± 0%105 ± 0%~(p=1.000 n=1+1)
DecodeJSON/FlowIDTableStaking.EpochTotalRewardsPaid-4158 ± 0%163 ± 0%~(p=1.000 n=1+1)
DecodeJSON/FlowIDTableStaking.NewWeeklyPayout-469.0 ± 0%71.0 ± 0%~(p=1.000 n=1+1)
DecodeJSON/FlowIDTableStaking.RewardsPaid-486.0 ± 0%89.0 ± 0%~(p=1.000 n=1+1)
DecodeJSON/FlowToken.TokensDeposited-495.0 ± 0%98.0 ± 0%~(p=1.000 n=1+1)
DecodeJSON/FlowToken.TokensDeposited_with_nil_receiver-485.0 ± 0%88.0 ± 0%~(p=1.000 n=1+1)
DecodeJSON/FlowToken.TokensMinted-470.0 ± 0%72.0 ± 0%~(p=1.000 n=1+1)
DecodeJSON/FlowToken.TokensWithdrawn-495.0 ± 0%98.0 ± 0%~(p=1.000 n=1+1)
EncodeBatchEventsCCF-41.00M ± 0%1.00M ± 0%~(p=1.000 n=1+1)
EncodeBatchEventsJSON-4902k ± 0%902k ± 0%~(p=1.000 n=1+1)
EncodeCCF/FlowFees.FeesDeducted-420.0 ± 0%20.0 ± 0%~(all equal)
EncodeCCF/FlowFees.TokensWithdrawn-420.0 ± 0%20.0 ± 0%~(all equal)
EncodeCCF/FlowIDTableStaking.DelegatorRewardsPaid-420.0 ± 0%20.0 ± 0%~(all equal)
EncodeCCF/FlowIDTableStaking.EpochTotalRewardsPaid-420.0 ± 0%20.0 ± 0%~(all equal)
EncodeCCF/FlowIDTableStaking.NewWeeklyPayout-420.0 ± 0%20.0 ± 0%~(all equal)
EncodeCCF/FlowIDTableStaking.RewardsPaid-420.0 ± 0%20.0 ± 0%~(all equal)
EncodeCCF/FlowToken.TokensDeposited-421.0 ± 0%21.0 ± 0%~(all equal)
EncodeCCF/FlowToken.TokensDeposited_with_nil_receiver-421.0 ± 0%21.0 ± 0%~(all equal)
EncodeCCF/FlowToken.TokensMinted-420.0 ± 0%20.0 ± 0%~(all equal)
EncodeCCF/FlowToken.TokensWithdrawn-421.0 ± 0%21.0 ± 0%~(all equal)
EncodeJSON/FlowFees.FeesDeducted-420.0 ± 0%20.0 ± 0%~(all equal)
EncodeJSON/FlowFees.TokensWithdrawn-413.0 ± 0%13.0 ± 0%~(all equal)
EncodeJSON/FlowIDTableStaking.DelegatorRewardsPaid-417.0 ± 0%17.0 ± 0%~(all equal)
EncodeJSON/FlowIDTableStaking.EpochTotalRewardsPaid-426.0 ± 0%26.0 ± 0%~(all equal)
EncodeJSON/FlowIDTableStaking.NewWeeklyPayout-413.0 ± 0%13.0 ± 0%~(all equal)
EncodeJSON/FlowIDTableStaking.RewardsPaid-416.0 ± 0%16.0 ± 0%~(all equal)
EncodeJSON/FlowToken.TokensDeposited-420.0 ± 0%20.0 ± 0%~(all equal)
EncodeJSON/FlowToken.TokensDeposited_with_nil_receiver-415.0 ± 0%15.0 ± 0%~(all equal)
EncodeJSON/FlowToken.TokensMinted-414.0 ± 0%14.0 ± 0%~(all equal)
EncodeJSON/FlowToken.TokensWithdrawn-419.0 ± 0%19.0 ± 0%~(all equal)
pkg:github.com/onflow/cadence/runtime goos:linux goarch:amd64
ExportType/composite_type-43.00 ± 0%3.00 ± 0%~(all equal)
ExportType/simple_type-40.00 0.00 ~(all equal)
pkg:github.com/onflow/cadence/interpreter goos:linux goarch:amd64
ImperativeFib-4176 ± 0%176 ± 0%~(all equal)
InterpretRecursionFib-417.7k ± 0%17.7k ± 0%~(all equal)
NewInterpreter/new_interpreter-415.0 ± 0%15.0 ± 0%~(all equal)
NewInterpreter/new_sub-interpreter-44.00 ± 0%4.00 ± 0%~(all equal)
pkg:github.com/onflow/cadence/runtime goos:linux goarch:amd64
RuntimeFungibleTokenTransferInterpreter-42.93k ± 0%2.94k ± 0%~(p=1.000 n=1+1)
RuntimeFungibleTokenTransferVM-43.06k ± 0%3.07k ± 0%~(p=1.000 n=1+1)
RuntimeResourceDictionaryValues-436.7k ± 0%36.7k ± 0%~(p=1.000 n=1+1)
RuntimeResourceTracking-4159k ± 0%159k ± 0%~(p=1.000 n=1+1)
RuntimeScriptNoop-4113 ± 0%113 ± 0%~(all equal)
RuntimeVMInvokeContractImperativeFib-4213 ± 0%213 ± 0%~(all equal)
pkg:github.com/onflow/cadence/interpreter goos:linux goarch:amd64
ValueIsSubtypeOfSemaType-41.00 ± 0%1.00 ± 0%~(all equal)
 

@turbolent
Copy link
Member

Keeping a path makes sense. However, keep the path as "elements" instead of strings. This avoids string formatting / allocation during the happy path.

Instead of pathContext []string, have a pathContext []pathElement, where pathElement is an interface:

type pathElement interface {
	Append(io.Writer)
}

then for example instead of

d.pushPath("array[" + strconv.Itoa(i) + "]")
d.pushPath(indexPathElement(i))

and indexPathElement defined as:

type indexPathElement int

var _ pathElement = indexPathElement(0)

func (e indexPathElement) Append(w io.Writer) {
	_, _ = fmt.Fprintf("array:%d", int(e))
}

Also, in getPathString, use a strings.Builder

Follow-up to address code review feedback from @turbolent.

Changes:
- Introduced pathElement interface with Append(io.Writer) method
- Created specific path element types:
  - indexPathElement: for array indices
  - fieldPathElement: for struct/resource/event field names
  - staticPathElement: for static strings like 'type'
- Updated getPathString() to use strings.Builder for efficient concatenation
- Changed pathContext from []string to []pathElement

This optimization avoids string formatting and allocation during the happy path
(successful decoding), only building the path string when an error occurs.

Addresses: #4288 (comment)
@devin-ai-integration
Copy link
Contributor Author

Thanks for the feedback! I've optimized the implementation to avoid string allocations during the happy path.

Changes made:

  • Introduced pathElement interface with Append(io.Writer) method
  • Created specific path element types:
    • indexPathElement for array indices
    • fieldPathElement for struct/resource/event field names
    • staticPathElement for static strings like "type"
  • Updated getPathString() to use strings.Builder for efficient concatenation
  • Changed pathContext from []string to []pathElement

This way, path construction only happens when an error occurs, not during successful decoding.

@turbolent
Copy link
Member

Add tests

Added comprehensive tests to verify that path context is correctly included
in error messages when JSON decoding fails.

Test cases cover:
- Missing property at root level
- Missing property in array elements
- Missing property in nested array elements
- Missing property in struct fields
- Missing property in type definitions
- Missing property in complex nested structures (array[0].field:nested)

All tests verify that the error message contains both the property name
and the path context showing where in the JSON structure the error occurred.
@devin-ai-integration
Copy link
Contributor Author

Added comprehensive tests for the error context feature in TestDecodeErrorContext.

The tests cover various error scenarios:

  • Missing properties at root level
  • Missing properties in array elements (simple and nested)
  • Missing properties in struct fields
  • Missing properties in type definitions
  • Complex nested paths (e.g., array[0].field:nested)

All tests verify that error messages correctly include the path context.

@turbolent
Copy link
Member

@bjartek How does this look? Would this be helpful? Does the path format look useful?

@bjartek
Copy link
Contributor

bjartek commented Oct 14, 2025

Yes wonderful!

@turbolent
Copy link
Member

Refactor all decode methods like this (dictionary, parameters, etc.) to use the context versions.

Comprehensive refactoring to consistently use GetWithContext, GetStringWithContext,
GetSliceWithContext, GetBoolWithContext, and GetValueWithContext throughout all
decode methods.

This ensures path context is available for all decoding errors, not just in a few
select locations. Now any missing property error will include context about where
in the JSON structure the error occurred.

Updated methods:
- decodeKeyValuePair
- decodeComposite
- decodeInclusiveRange
- decodePath
- decodeFunction
- decodeTypeParameter
- decodeParameter
- decodeFieldType
- decodeAuthorization
- decodeNominalType
- decodeType
- decodeTypeValue
- decodeCapability

Addresses: #4288 (review)
@devin-ai-integration
Copy link
Contributor Author

Refactored all decode methods to consistently use the *WithContext versions.

All obj.Get(), obj.GetString(), obj.GetBool(), obj.GetSlice(), and obj.GetValue() calls have been replaced with their *WithContext counterparts throughout the entire decode pipeline.

This ensures that any missing property error in any decode method will include path context showing where in the JSON structure the error occurred.

All existing tests pass with the changes.

- Add path tracking system using pathElement interface
- Implement indexPathElement, fieldPathElement, staticPathElement
- Refactor all decode methods to use *WithContext helpers
- Remove non-context Get* helper methods
- Add comprehensive error context tests

Closes #4209
@turbolent
Copy link
Member

Replaced by #4302

@turbolent turbolent closed this Oct 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Better error message when failing to decode cadence json

2 participants