Skip to content

feat(ast): add NodeFrom and Node.Unmarshal for struct/Node conversion#890

Open
blueberrycongee wants to merge 3 commits intobytedance:mainfrom
blueberrycongee:feature/issue-733-node-convert
Open

feat(ast): add NodeFrom and Node.Unmarshal for struct/Node conversion#890
blueberrycongee wants to merge 3 commits intobytedance:mainfrom
blueberrycongee:feature/issue-733-node-convert

Conversation

@blueberrycongee
Copy link
Copy Markdown

What type of PR is this?

feat: A new feature

Check the PR title.

  • This PR title match the format: <type>(optional scope): <description>
  • The description of this PR title is user-oriented and clear enough for others to understand.
  • Attach the PR updating the user documentation if the current PR requires user awareness at the usage level.

(Optional) Translate the PR title into Chinese.

新增 NodeFrom 和 Node.Unmarshal 方法,支持 Go 结构体与 ast.Node 的便捷互转

(Optional) More detailed description for this PR

en:

This PR adds two convenience APIs for converting between Go values and ast.Node:

  • NodeFrom(v interface{}) (Node, error) - Convert any Go value to a fully-parsed ast.Node
  • (*Node).Unmarshal(v interface{}) error - Decode an ast.Node back to a Go value

Unlike NewAny(), the Node returned by NodeFrom has a fully-parsed structure that supports all Get/Set operations.

Benchmark Results:

Operation Time Memory Allocs
NodeFrom (struct→Node) 1318 ns/op 1769 B/op 5
Node.Unmarshal (Node→struct) 1642 ns/op 778 B/op 6
Manual way (Marshal+Parse) 1233 ns/op 1742 B/op 5

The new API has ~7% overhead compared to manual approach, but provides a much cleaner interface.

zh(optional):

本 PR 新增两个便捷 API,用于 Go 值与 ast.Node 之间的转换:

  • NodeFrom(v interface{}) - 将任意 Go 值转换为完整可操作的 ast.Node
  • (*Node).Unmarshal(v interface{}) - 将 ast.Node 解码回 Go 值

NewAny() 不同,NodeFrom 返回的 Node 是完整解析的,支持所有 Get/Set 操作。

性能开销约 7%,换来更简洁的 API。

(Optional) Which issue(s) this PR fixes:

Fixes #733

(optional) The PR that updates user documentation:

N/A

@blueberrycongee blueberrycongee changed the title Feature/issue 733 node convert feat(ast): add NodeFrom and Node.Unmarshal for struct/Node conversion Dec 21, 2025
@AsterDY
Copy link
Copy Markdown
Collaborator

AsterDY commented Dec 22, 2025

please move unrelated commit to another PR

Comment thread ast/convert.go
// node, _ := sonic.Get(jsonBytes, "user")
// var user User
// err := node.Unmarshal(&user)
func (self *Node) Unmarshal(v interface{}) error {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If only such ineffecient implementation, Node.MarshalJSON()+ sonic.Unmarshal is enough. Consider using reflect to bind Node to Struct in place

Comment thread ast/convert.go
// }
// node, err := ast.NodeFrom(User{Name: "Alice", Age: 30})
// name, _ := node.Get("name").String() // "Alice"
func NodeFrom(v interface{}) (Node, error) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sonic.Marshal + Node.UnmarshalJSON() is simple enough. To get better performance, consider using reflect to bind Struct to Node in place

@blueberrycongee blueberrycongee force-pushed the feature/issue-733-node-convert branch from e6a5fcf to dfaab7c Compare December 24, 2025 12:58
@blueberrycongee
Copy link
Copy Markdown
Author

Thanks for the review! Here's what I've done:

NodeFrom: I tested a pure reflection approach, but it was 2.6x slower than the JIT approach (3200 ns vs 1230 ns). Since sonic's JIT encoder/parser is highly optimized, I kept using Marshal + Parse.

Unmarshal: Implemented with in-place reflection as suggested. It directly maps Node values to struct fields without intermediate JSON bytes, saving 72% memory (224B vs 794B).

Benchmark results:

NodeFrom: ~1237 ns/op (same as manual approach)
Unmarshal: ~1982 ns/op, 224 B/op (72% less memory than manual)
Should I continue pursuing reflection for NodeFrom despite the performance regression?

…bytedance#733)

- Add NodeFrom() to convert any Go value to a fully-parsed ast.Node

- Add Node.Unmarshal() to decode Node back to Go value

- Support both sonic (amd64/arm64) and encoding/json (fallback) implementations

- Add comprehensive tests and benchmarks
- NodeFrom: use JIT (Marshal+Parse) for best performance
- Unmarshal: use in-place reflection, saves 72% memory
- Fix uint negative number handling
- Add string tag support
- Simplify comments to match sonic code style
- Add comparison benchmarks
@blueberrycongee blueberrycongee force-pushed the feature/issue-733-node-convert branch from dfaab7c to 25f9ab1 Compare February 19, 2026 16:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Provide methods to parse to/from ast.Node from structures

2 participants