Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSON Schema: Add title for Literal, ECMA regexes, and null type in const #330

Merged
merged 2 commits into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 39 additions & 4 deletions schema/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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"):
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
27 changes: 24 additions & 3 deletions test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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") == {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
)
Expand All @@ -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",
}
Expand Down