From 829427375b1d9fa98c03a514c4ff5f2c1be376ce Mon Sep 17 00:00:00 2001 From: Denis Blanchette Date: Thu, 20 Feb 2025 16:58:00 -0500 Subject: [PATCH 1/2] feat: [JSON Schema] Add title for Literal fix: [JSON Schema] attempt to make regexes ECMA-compliant fix: [JSON Schema] Use "type": "null" instead of "const": "null" --- schema/__init__.py | 36 ++++++++++++++++++++++++++++++++---- test_schema.py | 20 +++++++++++++++++--- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/schema/__init__.py b/schema/__init__.py index 31bd71b..7931636 100644 --- a/schema/__init__.py +++ b/schema/__init__.py @@ -590,6 +590,7 @@ def json_schema( def _json_schema( schema: "Schema", is_main_schema: bool = True, + title: Union[str, None] = None, description: Union[str, None] = None, allow_reference: bool = True, ) -> Dict[str, Any]: @@ -654,6 +655,8 @@ def _to_schema(s: Any, ignore_extra_keys: bool) -> Schema: return_description: Union[str, None] = description or schema.description if return_description: return_schema["description"] = return_description + if title: + return_schema["title"] = title # Check if we have to create a common definition and use as reference if allow_reference and schema.as_reference: @@ -696,7 +699,11 @@ def _to_schema(s: Any, ignore_extra_keys: bool) -> Schema: ] # All values are simple, can use enum or const if len(or_values) == 1: - return_schema["const"] = _to_json_type(or_values[0]) + or_value = or_values[0] + if or_value is None: + return_schema["type"] = None + else: + return_schema["const"] = _to_json_type(or_value) return return_schema return_schema["enum"] = or_values else: @@ -728,10 +735,15 @@ def _to_schema(s: Any, ignore_extra_keys: bool) -> Schema: else: return_schema["allOf"] = all_of_values elif flavor == COMPARABLE: - return_schema["const"] = _to_json_type(s) + if s is None: + return_schema["type"] = None + else: + return_schema["const"] = _to_json_type(s) elif flavor == VALIDATOR and type(s) == Regex: return_schema["type"] = "string" - return_schema["pattern"] = s.pattern_str + # JSON schema uses ECMAScript regex syntax + # Translating one to another is not easy, but this should work for simple cases + return_schema["pattern"] = re.sub(r"\(\?P<[a-z\d_]+>", "(", s.pattern_str).replace("/", r"\/") else: if flavor != DICT: # If not handled, do not check @@ -753,6 +765,16 @@ def _key_allows_additional_properties(key: Any) -> bool: return key == str or key == object + def _get_key_title(key: Any) -> Union[str, None]: + """Get the title associated to a key (as specified in a Literal object). Return None if not a Literal""" + if isinstance(key, Optional): + return _get_key_title(key.schema) + + if isinstance(key, Literal): + return key.title + + return None + def _get_key_description(key: Any) -> Union[str, None]: """Get the description associated to a key (as specified in a Literal object). Return None if not a Literal""" if isinstance(key, Optional): @@ -786,6 +808,7 @@ def _get_key_name(key: Any) -> Any: expanded_schema[key_name] = _json_schema( sub_schema, is_main_schema=False, + title=_get_key_title(key), description=_get_key_description(key), ) if isinstance(key, Optional) and hasattr(key, "default"): @@ -888,9 +911,10 @@ def _default_function(nkey: Any, data: Any, error: Any) -> NoReturn: class Literal: - def __init__(self, value: Any, description: Union[str, None] = None) -> None: + def __init__(self, value: Any, description: Union[str, None] = None, title: Union[str, None] = None) -> None: self._schema: Any = value self._description: Union[str, None] = description + self._title: Union[str, None] = title def __str__(self) -> str: return str(self._schema) @@ -902,6 +926,10 @@ def __repr__(self) -> str: def description(self) -> Union[str, None]: return self._description + @property + def title(self) -> Union[str, None]: + return self._title + @property def schema(self) -> Any: return self._schema diff --git a/test_schema.py b/test_schema.py index 2d7a495..7b408f3 100644 --- a/test_schema.py +++ b/test_schema.py @@ -1077,6 +1077,19 @@ def test_json_schema_regex(): "type": "object", } +def test_json_schema_ecma_compliant_regex(): + s = Schema({Optional("username"): Regex("^(?P[a-zA-Z_][a-zA-Z0-9_]*)/$")}) + assert s.json_schema("my-id") == { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "my-id", + "properties": { + "username": {"type": "string", "pattern": "^([a-zA-Z_][a-zA-Z0-9_]*)\/$"} + }, + "required": [], + "additionalProperties": False, + "type": "object", + } + def test_json_schema_or_types(): s = Schema({"test": Or(str, int)}) @@ -1132,7 +1145,7 @@ def test_json_schema_const_is_none(): assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", - "properties": {"test": {"const": None}}, + "properties": {"test": {"type": None}}, "required": ["test"], "additionalProperties": False, "type": "object", @@ -1399,9 +1412,9 @@ def test_json_schema_dict_type(): } -def test_json_schema_title_and_description(): +def test_regex_json_schema(): s = Schema( - {Literal("productId", description="The unique identifier for a product"): int}, + {Literal("productId", title="Product ID", description="The unique identifier for a product"): int}, name="Product", description="A product in the catalog", ) @@ -1412,6 +1425,7 @@ def test_json_schema_title_and_description(): "description": "A product in the catalog", "properties": { "productId": { + "title": "Product ID", "description": "The unique identifier for a product", "type": "integer", } From 003595fbdd0b43f32fd47fed21252613971b9f13 Mon Sep 17 00:00:00 2001 From: Denis Blanchette Date: Thu, 20 Feb 2025 17:02:56 -0500 Subject: [PATCH 2/2] fix: ruff format --- schema/__init__.py | 11 +++++++++-- test_schema.py | 9 ++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/schema/__init__.py b/schema/__init__.py index 7931636..346e367 100644 --- a/schema/__init__.py +++ b/schema/__init__.py @@ -743,7 +743,9 @@ def _to_schema(s: Any, ignore_extra_keys: bool) -> Schema: return_schema["type"] = "string" # JSON schema uses ECMAScript regex syntax # Translating one to another is not easy, but this should work for simple cases - return_schema["pattern"] = re.sub(r"\(\?P<[a-z\d_]+>", "(", s.pattern_str).replace("/", r"\/") + return_schema["pattern"] = re.sub( + r"\(\?P<[a-z\d_]+>", "(", s.pattern_str + ).replace("/", r"\/") else: if flavor != DICT: # If not handled, do not check @@ -911,7 +913,12 @@ def _default_function(nkey: Any, data: Any, error: Any) -> NoReturn: class Literal: - def __init__(self, value: Any, description: Union[str, None] = None, title: Union[str, None] = None) -> None: + def __init__( + self, + value: Any, + description: Union[str, None] = None, + title: Union[str, None] = None, + ) -> None: self._schema: Any = value self._description: Union[str, None] = description self._title: Union[str, None] = title diff --git a/test_schema.py b/test_schema.py index 7b408f3..a129aed 100644 --- a/test_schema.py +++ b/test_schema.py @@ -1077,6 +1077,7 @@ def test_json_schema_regex(): "type": "object", } + def test_json_schema_ecma_compliant_regex(): s = Schema({Optional("username"): Regex("^(?P[a-zA-Z_][a-zA-Z0-9_]*)/$")}) assert s.json_schema("my-id") == { @@ -1414,7 +1415,13 @@ def test_json_schema_dict_type(): def test_regex_json_schema(): s = Schema( - {Literal("productId", title="Product ID", description="The unique identifier for a product"): int}, + { + Literal( + "productId", + title="Product ID", + description="The unique identifier for a product", + ): int + }, name="Product", description="A product in the catalog", )