diff --git a/jsonschema/exceptions.py b/jsonschema/exceptions.py index 083a423f7..d955e356e 100644 --- a/jsonschema/exceptions.py +++ b/jsonschema/exceptions.py @@ -8,6 +8,7 @@ from textwrap import dedent, indent from typing import TYPE_CHECKING, Any, ClassVar import heapq +import re import warnings from attrs import define @@ -23,6 +24,8 @@ WEAK_MATCHES: frozenset[str] = frozenset(["anyOf", "oneOf"]) STRONG_MATCHES: frozenset[str] = frozenset() +_JSON_PATH_COMPATIBLE_PROPERTY_PATTERN = re.compile("^[a-zA-Z][a-zA-Z0-9_]*$") + _unset = _utils.Unset() @@ -152,8 +155,11 @@ def json_path(self) -> str: for elem in self.absolute_path: if isinstance(elem, int): path += "[" + str(elem) + "]" - else: + elif _JSON_PATH_COMPATIBLE_PROPERTY_PATTERN.match(elem): path += "." + elem + else: + escaped_elem = elem.replace("\\", "\\\\").replace("'", r"\'") + path += "['" + escaped_elem + "']" return path def _set( diff --git a/jsonschema/tests/test_exceptions.py b/jsonschema/tests/test_exceptions.py index 69114e182..8d515a998 100644 --- a/jsonschema/tests/test_exceptions.py +++ b/jsonschema/tests/test_exceptions.py @@ -1,6 +1,8 @@ from unittest import TestCase import textwrap +import jsonpath_ng + from jsonschema import exceptions from jsonschema.validators import _LATEST_VERSION @@ -700,3 +702,58 @@ class TestHashable(TestCase): def test_hashable(self): {exceptions.ValidationError("")} {exceptions.SchemaError("")} + + +class TestJsonPathRendering(TestCase): + def validate_json_path_rendering(self, property_name, expected_path): + error = exceptions.ValidationError( + path=[property_name], + message="1", + validator="foo", + instance="i1", + ) + + rendered_json_path = error.json_path + self.assertEqual(rendered_json_path, expected_path) + + re_parsed_name = jsonpath_ng.parse(rendered_json_path).right.fields[0] + self.assertEqual(re_parsed_name, property_name) + + def test_basic(self): + self.validate_json_path_rendering("x", "$.x") + + def test_empty(self): + self.validate_json_path_rendering("", "$['']") + + def test_number(self): + self.validate_json_path_rendering("1", "$['1']") + + def test_period(self): + self.validate_json_path_rendering(".", "$['.']") + + def test_single_quote(self): + self.validate_json_path_rendering("'", r"$['\'']") + + def test_space(self): + self.validate_json_path_rendering(" ", "$[' ']") + + def test_backslash(self): + self.validate_json_path_rendering("\\", r"$['\\']") + + def test_backslash_single_quote(self): + self.validate_json_path_rendering(r"\'", r"$['\\\'']") + + def test_underscore(self): + self.validate_json_path_rendering("_", r"$['_']") + + def test_double_quote(self): + self.validate_json_path_rendering('"', """$['"']""") + + def test_hyphen(self): + self.validate_json_path_rendering("-", "$['-']") + + def test_json_path_injection(self): + self.validate_json_path_rendering("a[0]", "$['a[0]']") + + def test_open_bracket(self): + self.validate_json_path_rendering("[", "$['[']") diff --git a/noxfile.py b/noxfile.py index 2f750385c..f9869a35c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -61,6 +61,7 @@ def tests(session, installable): env = dict(JSON_SCHEMA_TEST_SUITE=str(ROOT / "json")) session.install("virtue", installable) + session.install("jsonpath-ng", installable) if session.posargs and session.posargs[0] == "coverage": if len(session.posargs) > 1 and session.posargs[1] == "github":