Fist Initial and Working Version of JsonSchema to Pydantic
This commit is contained in:
@@ -1 +1 @@
|
||||
from .jsonschema_to_pydantic import jsonschema_to_pydantic
|
||||
from .jsonschema_to_pydantic import ModelSchemaBuilder
|
||||
|
||||
@@ -1,8 +1,138 @@
|
||||
from jsonschema_pydantic.types import JSONSchema
|
||||
from jsonschema.exceptions import SchemaError
|
||||
from jsonschema.protocols import Validator
|
||||
from pydantic import create_model
|
||||
from pydantic.fields import Field
|
||||
|
||||
from typing import Any, Dict
|
||||
import warnings
|
||||
from typing import Type
|
||||
|
||||
_base_type_mappings = {
|
||||
"string": str,
|
||||
"number": float,
|
||||
"integer": int,
|
||||
"boolean": bool,
|
||||
"array": ...,
|
||||
"object": ...,
|
||||
}
|
||||
|
||||
|
||||
def jsonschema_to_pydantic(
|
||||
schema: JSONSchema,
|
||||
): ...
|
||||
class ModelSchemaBuilder:
|
||||
@staticmethod
|
||||
def build(
|
||||
schema: dict,
|
||||
):
|
||||
try:
|
||||
Validator.check_schema(schema)
|
||||
except SchemaError as e:
|
||||
raise ValueError(f"Invalid JSON Schema: {e}")
|
||||
|
||||
if schema["type"] != "object":
|
||||
raise TypeError(
|
||||
f"Invalid JSON Schema: {schema['type']}. Only 'object' can be converted to Pydantic models."
|
||||
)
|
||||
|
||||
return ModelSchemaBuilder._build_model_from_properties(
|
||||
schema["title"], schema["properties"], schema.get("required", [])
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _parse_properties(
|
||||
properties: dict, required_keys=None
|
||||
) -> dict[str, tuple[type, Field]]:
|
||||
required_keys = required_keys or []
|
||||
|
||||
fields = {}
|
||||
for name, prop in properties.items():
|
||||
fields[name] = ModelSchemaBuilder._build_field(name, prop, required_keys)
|
||||
|
||||
return fields
|
||||
|
||||
@staticmethod
|
||||
def _build_field(
|
||||
name, properties: dict, required_keys: list[str]
|
||||
) -> tuple[type, Field]:
|
||||
_field_type = None
|
||||
_field_args = {}
|
||||
|
||||
match properties["type"]:
|
||||
case "object":
|
||||
_field_type, _field_args = ModelSchemaBuilder._build_field_object(
|
||||
name, properties
|
||||
)
|
||||
case "array":
|
||||
_field_type, _field_args = ModelSchemaBuilder._build_field_array(
|
||||
name, properties
|
||||
)
|
||||
case "string":
|
||||
_field_type, _field_args = ModelSchemaBuilder._build_field_string(
|
||||
properties
|
||||
)
|
||||
case "boolean":
|
||||
_field_type, _field_args = ModelSchemaBuilder._build_field_boolean(
|
||||
properties
|
||||
)
|
||||
case "integer":
|
||||
_field_type, _field_args = ModelSchemaBuilder._build_field_int(
|
||||
properties
|
||||
)
|
||||
case "number":
|
||||
_field_type, _field_args = ModelSchemaBuilder._build_field_float(
|
||||
properties
|
||||
)
|
||||
case _:
|
||||
raise ValueError(f"Unsupported type: {properties['type']}")
|
||||
|
||||
if description := properties.get("description"):
|
||||
_field_args["description"] = description
|
||||
else:
|
||||
warnings.warn(
|
||||
f"Property {name} is missing a description. We highly recommend adding one."
|
||||
)
|
||||
|
||||
_default_value = ... if name in required_keys else None
|
||||
return _field_type, Field(_default_value, *_field_args)
|
||||
|
||||
@staticmethod
|
||||
def _build_field_object(name, properties: dict) -> tuple[type, dict[str, any]]:
|
||||
_field_type = ModelSchemaBuilder._build_model_from_properties(
|
||||
name, properties["properties"], properties.get("required", [])
|
||||
)
|
||||
return _field_type, {}
|
||||
|
||||
@staticmethod
|
||||
def _build_field_array(name, properties: dict) -> tuple[type, dict[str, any]]:
|
||||
_item_type = properties["items"]["type"]
|
||||
if _item_type == "object":
|
||||
_item_type = ModelSchemaBuilder._build_model_from_properties(
|
||||
name, properties["items"]["properties"]
|
||||
)
|
||||
else:
|
||||
_item_type = _base_type_mappings[_item_type]
|
||||
|
||||
return list[_item_type], {}
|
||||
|
||||
@staticmethod
|
||||
def _build_field_string(properties: dict) -> tuple[type, dict[str, any]]:
|
||||
return str, {}
|
||||
|
||||
@staticmethod
|
||||
def _build_field_boolean(properties: dict) -> tuple[type, dict[str, any]]:
|
||||
return bool, {}
|
||||
|
||||
@staticmethod
|
||||
def _build_field_int(properties: dict) -> tuple[type, dict[str, any]]:
|
||||
return int, {}
|
||||
|
||||
@staticmethod
|
||||
def _build_field_float(properties: dict) -> tuple[type, dict[str, any]]:
|
||||
return float, {}
|
||||
|
||||
@staticmethod
|
||||
def _build_model_from_properties(
|
||||
model_name: str, model_properties: dict, required_keys: list[str]
|
||||
) -> Type:
|
||||
properties = ModelSchemaBuilder._parse_properties(
|
||||
model_properties, required_keys
|
||||
)
|
||||
|
||||
return create_model(model_name, **properties)
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
from typing import Dict, List, Literal, NotRequired, TypedDict, Union
|
||||
|
||||
JSONSchemaType = Union[
|
||||
Literal["string", "number", "integer", "boolean", "array", "object", "null"],
|
||||
List[Literal["string", "number", "integer", "boolean", "array", "object", "null"]],
|
||||
]
|
||||
|
||||
|
||||
JSONSchemaProperty = TypedDict(
|
||||
"JSONSchemaProperty",
|
||||
{
|
||||
"type": JSONSchemaType,
|
||||
# Array-related properties
|
||||
"items": NotRequired[Union["JSONSchemaProperty", List["JSONSchemaProperty"]]],
|
||||
"minItems": NotRequired[int],
|
||||
"maxItems": NotRequired[int],
|
||||
"uniqueItems": NotRequired[bool],
|
||||
# String constraints
|
||||
"minLength": NotRequired[int],
|
||||
"maxLength": NotRequired[int],
|
||||
"pattern": NotRequired[str],
|
||||
"format": NotRequired[str],
|
||||
# Number constraints
|
||||
"minimum": NotRequired[Union[int, float]],
|
||||
"maximum": NotRequired[Union[int, float]],
|
||||
"exclusiveMinimum": NotRequired[Union[int, float]],
|
||||
"exclusiveMaximum": NotRequired[Union[int, float]],
|
||||
"multipleOf": NotRequired[Union[int, float]],
|
||||
# Enumerations
|
||||
"enum": NotRequired[List[Union[str, int, float, bool, None]]],
|
||||
"const": NotRequired[Union[str, int, float, bool, None]],
|
||||
# Conditional Subschemas
|
||||
"allOf": NotRequired[List["JSONSchemaProperty"]],
|
||||
"anyOf": NotRequired[List["JSONSchemaProperty"]],
|
||||
"oneOf": NotRequired[List["JSONSchemaProperty"]],
|
||||
"not_": NotRequired["JSONSchemaProperty"],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# Implementing JSONSchema TypedDict
|
||||
JSONSchema = TypedDict(
|
||||
"JSONSchema",
|
||||
{
|
||||
# General metadata
|
||||
"$schema": str,
|
||||
"$id": str,
|
||||
"title": str,
|
||||
"description": str,
|
||||
# Basic Type Definition
|
||||
"type": JSONSchemaType,
|
||||
# Object-related properties
|
||||
"properties": NotRequired[Dict[str, JSONSchemaProperty]],
|
||||
"required": NotRequired[List[str]],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# Implementing Pydantic Validator
|
||||
|
||||
JSONSchemaValidator = TypeAdapter(JSONSchema)
|
||||
Reference in New Issue
Block a user