1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
|
import enum
from textwrap import indent
from typing import Optional
from jsonschema import Draft7Validator, ValidationError
from pydantic import BaseModel
class JSONSchema(BaseModel):
class Type(str, enum.Enum):
STRING = "string"
ARRAY = "array"
OBJECT = "object"
NUMBER = "number"
INTEGER = "integer"
BOOLEAN = "boolean"
# TODO: add docstrings
description: Optional[str] = None
type: Optional[Type] = None
enum: Optional[list] = None
required: bool = False
items: Optional["JSONSchema"] = None
properties: Optional[dict[str, "JSONSchema"]] = None
minimum: Optional[int | float] = None
maximum: Optional[int | float] = None
minItems: Optional[int] = None
maxItems: Optional[int] = None
def to_dict(self) -> dict:
schema: dict = {
"type": self.type.value if self.type else None,
"description": self.description,
}
if self.type == "array":
if self.items:
schema["items"] = self.items.to_dict()
schema["minItems"] = self.minItems
schema["maxItems"] = self.maxItems
elif self.type == "object":
if self.properties:
schema["properties"] = {
name: prop.to_dict() for name, prop in self.properties.items()
}
schema["required"] = [
name for name, prop in self.properties.items() if prop.required
]
elif self.enum:
schema["enum"] = self.enum
else:
schema["minumum"] = self.minimum
schema["maximum"] = self.maximum
schema = {k: v for k, v in schema.items() if v is not None}
return schema
@staticmethod
def from_dict(schema: dict) -> "JSONSchema":
return JSONSchema(
description=schema.get("description"),
type=schema["type"],
enum=schema["enum"] if "enum" in schema else None,
items=JSONSchema.from_dict(schema["items"]) if "items" in schema else None,
properties=JSONSchema.parse_properties(schema)
if schema["type"] == "object"
else None,
minimum=schema.get("minimum"),
maximum=schema.get("maximum"),
minItems=schema.get("minItems"),
maxItems=schema.get("maxItems"),
)
@staticmethod
def parse_properties(schema_node: dict) -> dict[str, "JSONSchema"]:
properties = (
{k: JSONSchema.from_dict(v) for k, v in schema_node["properties"].items()}
if "properties" in schema_node
else {}
)
if "required" in schema_node:
for k, v in properties.items():
v.required = k in schema_node["required"]
return properties
def validate_object(self, object: object) -> tuple[bool, list[ValidationError]]:
"""
Validates an object or a value against the JSONSchema.
Params:
object: The value/object to validate.
schema (JSONSchema): The JSONSchema to validate against.
Returns:
bool: Indicates whether the given value or object is valid for the schema.
list[ValidationError]: The issues with the value or object (if any).
"""
validator = Draft7Validator(self.to_dict())
if errors := sorted(validator.iter_errors(object), key=lambda e: e.path):
return False, errors
return True, []
def to_typescript_object_interface(self, interface_name: str = "") -> str:
if self.type != JSONSchema.Type.OBJECT:
raise NotImplementedError("Only `object` schemas are supported")
if self.properties:
attributes: list[str] = []
for name, property in self.properties.items():
if property.description:
attributes.append(f"// {property.description}")
attributes.append(f"{name}: {property.typescript_type};")
attributes_string = "\n".join(attributes)
else:
attributes_string = "[key: string]: any"
return (
f"interface {interface_name} " if interface_name else ""
) + f"{{\n{indent(attributes_string, ' ')}\n}}"
@property
def typescript_type(self) -> str:
if self.type == JSONSchema.Type.BOOLEAN:
return "boolean"
elif self.type in {JSONSchema.Type.INTEGER, JSONSchema.Type.NUMBER}:
return "number"
elif self.type == JSONSchema.Type.STRING:
return "string"
elif self.type == JSONSchema.Type.ARRAY:
return f"Array<{self.items.typescript_type}>" if self.items else "Array"
elif self.type == JSONSchema.Type.OBJECT:
if not self.properties:
return "Record<string, any>"
return self.to_typescript_object_interface()
elif self.enum:
return " | ".join(repr(v) for v in self.enum)
else:
raise NotImplementedError(
f"JSONSchema.typescript_type does not support Type.{self.type.name} yet"
)
|