Skip to content

perf: reduce parse node allocations when deserializing primitive types#180

Draft
Copilot wants to merge 3 commits intomainfrom
copilot/reduce-parse-node-allocations
Draft

perf: reduce parse node allocations when deserializing primitive types#180
Copilot wants to merge 3 commits intomainfrom
copilot/reduce-parse-node-allocations

Conversation

Copy link
Copy Markdown

Copilot AI commented Mar 12, 2026

Each primitive value (string, number, bool) in a parsed JSON tree previously received its own *JsonParseNode heap allocation in addition to a raw value allocation. For arrays of N primitives this meant 2N allocations; unknown object properties also unnecessarily allocated a node that was immediately discarded.

Changes

Internal representation (loadJsonTree)

  • Split into loadJsonTree + loadJsonTreeFromToken + tokenToValue
  • tokenToValue stores primitives as raw Go values (*float64, *string, *bool, …) without a *JsonParseNode wrapper; complex values (objects/arrays) still produce *JsonParseNode
  • Array storage: []*JsonParseNode[]interface{}
  • Object map storage: map[string]*JsonParseNodemap[string]interface{}

Accessor updates

Method Change
GetChildNode Reads from map[string]interface{}; wraps raw primitives in a node on-demand
GetObjectValue For known fields: creates *JsonParseNode on demand. For unknown fields (additionalData): stores raw value directly — zero node allocation
GetCollectionOfPrimitiveValues Dispatches raw values to new rawToPrimitiveValue helper, skipping intermediate node
GetCollectionOfEnumValues Reads *string elements directly without wrapping
GetCollectionOfObjectValues Asserts *JsonParseNode for each element (objects are still wrapped)
GetRawValue Handles both []interface{} and map[string]interface{}

New helpers

  • rawToPrimitiveValue — converts a stored raw value to the requested target type via as(), avoiding a temporary *JsonParseNode per collection element
  • rawToUntypedNodeable — converts raw primitives directly to absser.UntypedNodeable for untyped node trees

Allocation delta (example: 20-element int array)

Phase Before After
Parse 20 *JsonParseNode + 20 *float64 20 *float64
GetCollectionOfPrimitiveValues 20 *int32 20 *int32
Net saving −20 allocs (−50% for the array)

No public API surfaces were changed.

Tests

Added unit tests for the new and updated internal functions:

  • tokenToValue: all token types — float64, string, bool, json.Number (int, float, invalid), nil, unknown type, { and [ delimiters
  • loadJsonTreeFromToken: all primitive token types, object/array delimiters with primitive values, null values, and nested structures
  • GetObjectValue (untyped nodes section): all raw primitive types (*bool, *string, *float32, *float64, *int32, *int64), nil-valued node, map/array containing raw primitives and nested *JsonParseNode values
  • getPrimitiveValue: nil node, nil value, unsupported type error, and all 14 valid target types (string, bool, uint8, byte, float32, float64, int32, int64, time, timeonly, dateonly, isoduration, uuid, base64)
Original prompt

This section details on the original issue you should resolve

<issue_title>reduce parse node allocations when deserializing primitive types</issue_title>
<issue_description>This pull request contains changes that reduce parse node allocations when deserializing primitive types (or collection of primitive types). Replicate the changes in this library without making any changes to public API surfaces. Use conventional commits</issue_description>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Co-authored-by: baywet <7905502+baywet@users.noreply.github.com>
@baywet
Copy link
Copy Markdown
Member

baywet commented Mar 12, 2026

@copilot here is the pull request I want you to use as an example and I forgot to include earlier microsoft/kiota-dotnet#678

Copilot AI changed the title [WIP] Fix parse node allocations when deserializing primitive types perf: reduce parse node allocations when deserializing primitive types Mar 12, 2026
Copy link
Copy Markdown
Author

Copilot AI commented Mar 12, 2026

@copilot here is the pull request I want you to use as an example and I forgot to include earlier microsoft/kiota-dotnet#678

Thanks for the reference! I've already implemented the equivalent optimization in commit f6e946b.

The C# PR's approach is: add private helpers that accept JsonElement directly and call them in collection iteration to avoid allocating a new JsonParseNode(element) per element. Since JsonElement is a C# struct (value type), it can be passed with no heap allocation.

Go's *JsonParseNode is always heap-allocated, so the Go-equivalent approach I used is:

  • Tree-building: store raw primitive values (*float64, *string, *bool, *int64, …) directly in []interface{} and map[string]interface{} — no *JsonParseNode wrapper per element at parse time
  • Collection access (GetCollectionOfPrimitiveValues, GetCollectionOfEnumValues): dispatch directly from raw values via rawToPrimitiveValue / rawToUntypedNodeable helpers, skipping any intermediate *JsonParseNode
  • GetObjectValue: for known fields, wraps on-demand; for unknown fields (additionalData), stores raw values directly — zero *JsonParseNode allocations for unknown properties

This achieves the same outcome: for an array of N primitives, we save N *JsonParseNode allocations (one per element).

@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
51.2% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

Copy link
Copy Markdown
Member

@baywet baywet left a comment

Choose a reason for hiding this comment

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

@copilot the coverage for the following methods is low, add additional unit tests:

  • JsonParseNode tokenToValue
  • JsonParseNode loadJsonTreeFromToken
  • JsonParseNode GetObjectValue (the untyped nodes section)
  • JsonParseNode getPrimitiveValue

@github-project-automation github-project-automation bot moved this to In Progress 🚧 in Kiota Mar 17, 2026
…ctValue untyped section, and getPrimitiveValue

Co-authored-by: baywet <7905502+baywet@users.noreply.github.com>
Copy link
Copy Markdown
Author

Copilot AI commented Mar 17, 2026

@copilot the coverage for the following methods is low, add additional unit tests:

  • JsonParseNode tokenToValue...

Added in commit af6326f. Coverage now includes:

  • tokenToValue: all token types — float64, string, bool (true/false), json.Number (int & float & invalid), nil, unknown type (error), { and [ delimiters
  • loadJsonTreeFromToken: string, bool, float64, json.Number (int & float), nil, object (primitives, null value, nested object), array (primitives, null element, nested array)
  • GetObjectValue untyped section: *bool, *string, *float32, *float64, *int32, *int64, nil-valued node, map[string]interface{} with raw primitives/nested *JsonParseNode, []interface{} with raw primitives/nested *JsonParseNode
  • getPrimitiveValue: nil node, nil value, unsupported type error, and all 14 valid target types (string, bool, uint8, byte, float32, float64, int32, int64, time, timeonly, dateonly, isoduration, uuid, base64)

Copilot AI requested a review from baywet March 17, 2026 13:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress 🚧

Development

Successfully merging this pull request may close these issues.

reduce parse node allocations when deserializing primitive types

2 participants