[API Proposal]: Add Advance method on Utf8JsonReader

Open
#118,364 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
25/100
Issue type
Feature
Clarity
Mostly clear
Activity status
Stale
Tech stack
csharp
Domain
api, backend

Research direction

Start by reviewing the existing Utf8JsonReader API and the JsonSerializer.Deserialize(ref reader) usage shown in the proposal. Compare the single-target JsonPath and JsonPathReader alternatives, including behavior for multi-target paths. Done requires an agreed API shape and semantics; the issue does not name implementation files or tests.

Written by the indexing model from the issue text.

Description

api-suggestion area-System.Text.Json needs-further-triage
Background and motivation

In certain circumstances you may want to partially deserialize a json byte[] or pull out a slice of the json byte[]. If you know the object you can write a routine that can use Utf8JsonReader.Read to advance through the json structure until you get the spot you need but it would be nice if this was a generic routine.

I would imagine we would have to adopt a specific format to indicate where in the json you want to advance to and JsonPath seems like a good candidate. The main drawbacks to this is this Advance method implies a single item you are finding so JsonPaths like $.x.y[?(@.z<10)] wouldn't necessarily work. Single target JsonPaths would work though like $.x.y[0].z.

API Proposal

The API would be a new method / extension on Utf8JsonReader like this.

public ref struct Utf8JsonReader
{
    public bool Advance(string jsonPath)
}

We could have overloads that take ReadOnlySpan since this encoding to utf8 bytes will need to be done regardless to efficiently compare property name value spans between the JsonPath and the Utf8JsonReader. We could also consider a new JsonPathReader as an overload.

public ref struct JsonPathReader
{
    public bool Read();
    public JsonPathToken Current { get; }
    
}

public ref struct JsonPathToken
{
    public int TokenStartIndex { get; }
    public JsonPathTokenType TokenType { get; }
    public ReadOnlySpan<byte> ValueSpan { get; }

    public JsonPathToken(JsonPathTokenType tokenType, int tokenStartIndex, ReadOnlySpan<byte> valueSpan = default)
}
API Usage

Simple usage would be

byte[] json = GetJson();
Utf8JsonReader reader = new(json);
reader.Advance("$.x.y");
// reader is now at the PropertyName token of y

The Advance method would have no opinion on what the user can / should do after it advances but some usages would be.

Get sub json to partial deserialize

byte[] json = GetJson();
Utf8JsonReader reader = new(json);
reader.Advance("$.x.y");

// skip the PropertyName token for y
reader.Read();

// I can now partially deserialize this value only
MyModel model = JsonSerializer.Deserialize<MyModel>(ref reader);

Get the raw bytes of the json represented by the jsonPath

byte[] json = GetJson();
Utf8JsonReader reader = new(json);
reader.Advance("$.x.y");

// skip the PropertyName token for y
reader.Read();

long start = jsonReader.TokenStartIndex;
reader.Skip();
long end = jsonReader.BytesConsumed;

// I can now do things like send these raw bytes to a stream without ever needing to instantiate any objects
ReadOnlySpan<byte> partialJson = json.AsSpan((int)start, (int)(end - start));

Remove an item from a json byte array

byte[] json = GetJson();
Utf8JsonReader reader = new(json);
reader.Advance("$.x.y");

long endLeft = jsonReader.TokenStartIndex;
reader.Skip();
reader.Read();
long startRight = jsonReader.TokenStartIndex;

byte[] newJson = [.. json.AsSpan(0, (int)endLeft), .. json.AsSpan((int)startRight)];

There are many more scenarios these are just a couple that my projects have.

Alternative Designs

I think the tricky part here is the JsonPath. We can hide the implementation of JsonPathReader by just taking in string, byte[], ReadOnlySpan which might allow for some flexibility on implementation in the future.

Not sure if JsonPath is the right format to use given the fact that Advance is intended to work with single target JsonPath's but it is a well known format and it should be easy enough to convey in documentation / summary xml docs that its intended to work with single target only and if a multi-target is passed in it will stop on the first one and if you wanted to iterate through them all you could do.

byte[] json = GetJson();
Utf8JsonReader reader = new(json);
JsonPathReader pathReader = new("$.x.y[?(@.z<10)]");
while(reader.Advance(ref pathReader))
{
    //do something with each occurrence
}

An alternative to a new Advance method would be an overload to Read which took the JsonPath, but I feel like this makes the concept of Read more confusing.

public ref struct Utf8JsonReader
{
    public bool Read(string jsonPath)
}
Risks

No response

Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from dotnet/runtime

All issues in dotnet/runtime

Similar issues

More C# issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.