Skip to content

Conversation

@martonvago
Copy link
Contributor

Description

This PR adds support for the union JSON path operator for RequiredChecks.

To be able to do this more intelligently than a monster regex, I looked into how jsonpath represents a JSON path internally and used their terms/classification. This also allowed me to be more precise about what JSON paths we are not allowing for RequiredChecks.

Closes #193

Needs an in-depth review.

Checklist

  • Formatted Markdown
  • Ran just run-all

Comment on lines +97 to +98
@dataclass(frozen=True)
class TargetJsonPath:
Copy link
Contributor Author

@martonvago martonvago Nov 19, 2025

Choose a reason for hiding this comment

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

If we allow JSON path unions, one JSON path string can encode multiple completely different JSON paths. I added TargetJsonPath to keep track of these different JSON paths in the context of a RequiredCheck.

As before, I'm keeping track of the beginning of the path and the last field name separately, because I need access to them separately in apply. Otherwise, the general logic hasn't changed, but now we're running it on all JSON paths, not just the one.


def _jsonpath_to_targets(jsonpath: JSONPath) -> list[TargetJsonPath]:
"""Create a list of `TargetJsonPath`s from a `JSONPath`."""
if not jsonpath.segments:
Copy link
Contributor Author

@martonvago martonvago Nov 19, 2025

Choose a reason for hiding this comment

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

Segments are path parts, e.g., resources, *, name for $.resources[*].name

Comment on lines +117 to +121
if isinstance(last_segment, JSONPathRecursiveDescentSegment):
raise ValueError(
f"Cannot use `RequiredCheck` for the JSON path `{full_path}`"
" because it ends in the recursive descent (`..`) operator."
)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Non-final recursive descents are fine, see the tests for an example

" because it ends in the recursive descent (`..`) operator."
)

selectors = last_segment.selectors
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Selectors are things like field names, array indices, wildcards

Comment on lines +130 to +137
parent = "".join(_map(jsonpath.segments[:-1], lambda segment: str(segment)))
name_selectors = cast(tuple[NameSelector], selectors)
return _map(
name_selectors,
lambda selector: TargetJsonPath(
parent=str(compile(parent)), field=selector.name
),
)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

An easy way of constructing a string JSON path to the parent of the field is to take all segments except the last one (this is the field name), join them, and then compile them. Just joining them into a string without compiling them also creates a valid JSON path but it is in a slightly different notation.

Comment on lines +168 to +170
else:
first_path = cast(JSONPath, jsonpath.path)
paths = [first_path] + _map(jsonpath.paths, lambda path: path[1])
Copy link
Contributor Author

@martonvago martonvago Nov 19, 2025

Choose a reason for hiding this comment

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

This is where we handle CompoundJSONPaths, i.e., JSON paths with unions (or intersections).
The structure of CompoundJSONPaths is that they have the first member of the union as path and the rest in paths.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For intersections see #202

properties = example_package_properties()
properties["resources"][0]["licenses"] = [{"name": "odc-pddl"}]
required_check = RequiredCheck(
jsonpath="$..licenses[*].title",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Match all license titles in the object, no matter how nested the license is

]


def test_required_check_root():
Copy link
Contributor Author

Choose a reason for hiding this comment

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

More of a sanity check

"jsonpath",
[
"<><>bad.path",
"$",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Whether we allow the root node to be required or not is not really important. If there is no root node, something else will surely fail. Refactoring the Pydantic check made it easier to allow this.

"<><>bad.path",
"$",
"..*",
"created",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is just a shorthand for $.created. After the refactor this is easy to allow.

@martonvago martonvago moved this from Todo to In Progress in Iteration planning Nov 19, 2025
@martonvago martonvago moved this from In Progress to In Review in Iteration planning Nov 19, 2025
@martonvago martonvago marked this pull request as ready for review November 19, 2025 13:33
@martonvago martonvago requested a review from a team as a code owner November 19, 2025 13:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Review

Development

Successfully merging this pull request may close these issues.

[discussion]: Support union JSON path operator in RequiredCheck as well?

2 participants