Skip to content

Support Nested matching via JsonPathValue/logFieldJsonPath - #572

Open
FranAguilera wants to merge 1 commit into
mainfrom
franjam/handle-json-type-at-workflow-matcher
Open

Support Nested matching via JsonPathValue/logFieldJsonPath#572
FranAguilera wants to merge 1 commit into
mainfrom
franjam/handle-json-type-at-workflow-matcher

Conversation

@FranAguilera

@FranAguilera FranAguilera commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Adds support for nested json matching via the existing Leaf::JsonPathValue, for this initial prototype I'm proposing to use existing JSON string to extract the nested conditions

More details, regarding frotnend/rollout/performance etc in the internal PRD here

You can run local benchmark tests with make benchmark-json-matcher

This is used in bitdriftlabs/capture-sdk#1079

NOTE: Pending to flag new logic for json string

@FranAguilera
FranAguilera force-pushed the franjam/handle-json-type-at-workflow-matcher branch 3 times, most recently from 4cf0502 to 0f9ae96 Compare August 6, 2026 00:01
@FranAguilera FranAguilera changed the title Nested matching Support Nested matching via JsonPathValue/logFieldJsonPath Aug 6, 2026
@FranAguilera
FranAguilera marked this pull request as ready for review August 6, 2026 00:15
@FranAguilera FranAguilera reopened this Aug 6, 2026
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 6, 2026
@FranAguilera
FranAguilera force-pushed the franjam/handle-json-type-at-workflow-matcher branch 3 times, most recently from f73c2b3 to bc1e8fe Compare August 7, 2026 22:48
@FranAguilera
FranAguilera force-pushed the franjam/handle-json-type-at-workflow-matcher branch from bc1e8fe to 82565e4 Compare August 7, 2026 22:53
@FranAguilera
FranAguilera requested a review from snowp August 7, 2026 22:54
@bitdriftlabs bitdriftlabs unlocked this conversation Aug 7, 2026
// Existing SDK APIs send JSON as a string. Parsing is only reached from JsonPathValue matchers.
// TODO: Gate JSON string extraction with a remote workflow runtime flag.
if let Some(json) = value.as_str() {
return resolve_json_string_path(json, path);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Pending to add a killswitch flag here so we can disable remotely in case something is off

@snowp snowp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we need a depth bound on the parser to deal with bad input but other than that all my comments are just suggestions or style guidance, good job!

escaped: bool,
}

enum PathResult<'a> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this is just Option<Cow<'a, str>>, I don't think the type adds that much and prevents ? usage

Looking at how we do return Option<PathResult<_>> I'd either model it as

enum PathResult {
  Found(Cow<'a, str>,
  Missing,
  Invalid
}

if we want to understand the tri-state or just collapse it to an Option<Cow<'a, str>> for simplicity

}

#[test]
#[ignore = "run manually with --release -- --ignored --nocapture"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This would be better off as a criterion test - I just added one that we can use for reference. It handles the standard stuff like running it multiple times, percentiles etc. Not a blocker for this PR but it should make the benchmarking experience better

Self { input, pos: 0 }
}

fn walk_value(&mut self, path: &[JsonPathToken]) -> Option<PathResult<'a>> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With these kind of recursive parsers one thing we have to be careful about when we don't control the input is stack overflow. The simplest thing to do is to add a depth limit (say 128) to prevent going to deep, or switch over to an iterative approach that doesn't have the same per-depth overhead. I think a depth limit is reasonable so we can probably start there unless we believe someone really cares about super complex JSON objects

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

great idea, will add

}

#[cfg(test)]
mod tests {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Per the guidelines these would go in json_path_test.rs, try out https://github.com/bitdriftlabs/ai-instructions if you need to set up your agent and it should do this automatically

}

#[cfg(test)]
mod tests {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd probably add some more tests for correctness, there's a lot of test surface. The agent will do a good job here

Comment thread fuzz/src/json_path.rs
}

impl<'a> Arbitrary<'a> for JsonPathFuzzTestCase {
fn arbitrary(input: &mut Unstructured<'a>) -> arbitrary::Result<Self> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One thing you can do here to validate correctness via fuzzing is to separately implement this via serde_json - the implementation is much simpler and you get to validate that our more complex custom parser agrees with the simple implementation. Then you can let it run and catch any issues (or have the agent do it)

Love using a fuzzer for this!

fn resolve_json_path<'a>(value: &'a DataValue, path: &[JsonPathToken]) -> Option<Cow<'a, str>> {
// Existing SDK APIs send JSON as a string. Parsing is only reached from JsonPathValue matchers.
// TODO: Gate JSON string extraction with a remote workflow runtime flag.
if let Some(json) = value.as_str() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

just for code clarity I would do something like if let Some(maybe_json) ... as I think it more clearly captures that we aren't expecting JSON here but are opportunistically trying to extract a JSON value per the matcher

Comment thread fuzz/src/json_path.rs
}

impl<'a> Arbitrary<'a> for JsonPathFuzzTestCase {
fn arbitrary(input: &mut Unstructured<'a>) -> arbitrary::Result<Self> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't know if serde_json supports arbitrary out of the box but it would be interesting to have the agent 1) generate sensible JSON and 2) try to ensure that occasionally they match. If you look at the state fuzzer we do stuff like that to try to guide it to sane values

}
}

struct Parser<'a> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would add some docs on the perf implications of this, there's a few different things worth calling out when it needs to allocate vs not e.g. for escaped values

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.

2 participants