diff --git a/schema/__init__.py b/schema/__init__.py index 31bd71b..346e367 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,17 @@ 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 +767,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 +810,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 +913,15 @@ 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 +933,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..a129aed 100644 --- a/test_schema.py +++ b/test_schema.py @@ -1078,6 +1078,20 @@ def test_json_schema_regex(): } +def test_json_schema_ecma_compliant_regex(): + s = Schema({Optional("username"): Regex("^(?P<name>[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)}) assert s.json_schema("my-id") == { @@ -1132,7 +1146,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 +1413,15 @@ 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 +1432,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", }