Skip to content

Commit 07e2b72

Browse files
Expose public sort method for JSON schema generation (#10595)
Co-authored-by: Victorien <[email protected]>
1 parent 09ade35 commit 07e2b72

2 files changed

Lines changed: 96 additions & 20 deletions

File tree

docs/concepts/json_schema.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1414,6 +1414,68 @@ print(validation_schema)
14141414
"""
14151415
```
14161416

1417+
### JSON schema sorting
1418+
1419+
By default, Pydantic recursively sorts JSON schemas by alphabetically sorting keys. Notably, Pydantic skips sorting the values of the `properties` key,
1420+
to preserve the order of the fields as they were defined in the model.
1421+
1422+
If you would like to customize this behavior, you can override the `sort` method in your custom `GenerateJsonSchema` subclass. The below example
1423+
uses a no-op `sort` method to disable sorting entirely, which is reflected in the preserved order of the model fields and `json_schema_extra` keys:
1424+
1425+
```py
1426+
import json
1427+
from typing import Optional
1428+
1429+
from pydantic import BaseModel, Field
1430+
from pydantic.json_schema import GenerateJsonSchema, JsonSchemaValue
1431+
1432+
1433+
class MyGenerateJsonSchema(GenerateJsonSchema):
1434+
def sort(
1435+
self, value: JsonSchemaValue, parent_key: Optional[str] = None
1436+
) -> JsonSchemaValue:
1437+
"""No-op, we don't want to sort schema values at all."""
1438+
return value
1439+
1440+
1441+
class Bar(BaseModel):
1442+
c: str
1443+
b: str
1444+
a: str = Field(json_schema_extra={'c': 'hi', 'b': 'hello', 'a': 'world'})
1445+
1446+
1447+
json_schema = Bar.model_json_schema(schema_generator=MyGenerateJsonSchema)
1448+
print(json.dumps(json_schema, indent=2))
1449+
"""
1450+
{
1451+
"type": "object",
1452+
"properties": {
1453+
"c": {
1454+
"type": "string",
1455+
"title": "C"
1456+
},
1457+
"b": {
1458+
"type": "string",
1459+
"title": "B"
1460+
},
1461+
"a": {
1462+
"type": "string",
1463+
"c": "hi",
1464+
"b": "hello",
1465+
"a": "world",
1466+
"title": "A"
1467+
}
1468+
},
1469+
"required": [
1470+
"c",
1471+
"b",
1472+
"a"
1473+
],
1474+
"title": "Bar"
1475+
}
1476+
"""
1477+
```
1478+
14171479
## Customizing the `$ref`s in JSON Schema
14181480

14191481
The format of `$ref`s can be altered by calling [`model_json_schema()`][pydantic.main.BaseModel.model_json_schema]

pydantic/json_schema.py

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ def generate_definitions(
392392
json_schema = {'$defs': self.definitions}
393393
json_schema = definitions_remapping.remap_json_schema(json_schema)
394394
self._used = True
395-
return json_schemas_map, _sort_json_schema(json_schema['$defs']) # type: ignore
395+
return json_schemas_map, self.sort(json_schema['$defs']) # type: ignore
396396

397397
def generate(self, schema: CoreSchema, mode: JsonSchemaMode = 'validation') -> JsonSchemaValue:
398398
"""Generates a JSON schema for a specified schema in a specified mode.
@@ -441,7 +441,7 @@ def generate(self, schema: CoreSchema, mode: JsonSchemaMode = 'validation') -> J
441441
# json_schema['$schema'] = self.schema_dialect
442442

443443
self._used = True
444-
return _sort_json_schema(json_schema)
444+
return self.sort(json_schema)
445445

446446
def generate_inner(self, schema: CoreSchemaOrField) -> JsonSchemaValue: # noqa: C901
447447
"""Generates a JSON schema for a given core schema.
@@ -556,6 +556,38 @@ def new_handler_func(
556556
json_schema = populate_defs(schema, json_schema)
557557
return json_schema
558558

559+
def sort(self, value: JsonSchemaValue, parent_key: str | None = None) -> JsonSchemaValue:
560+
"""Override this method to customize the sorting of the JSON schema (e.g., don't sort at all, sort all keys unconditionally, etc.)
561+
562+
By default, alphabetically sort the keys in the JSON schema, skipping the 'properties' and 'default' keys to preserve field definition order.
563+
This sort is recursive, so it will sort all nested dictionaries as well.
564+
"""
565+
sorted_dict: dict[str, JsonSchemaValue] = {}
566+
keys = value.keys()
567+
if parent_key not in ('properties', 'default'):
568+
keys = sorted(keys)
569+
for key in keys:
570+
sorted_dict[key] = self._sort_recursive(value[key], parent_key=key)
571+
return sorted_dict
572+
573+
def _sort_recursive(self, value: Any, parent_key: str | None = None) -> Any:
574+
"""Recursively sort a JSON schema value."""
575+
if isinstance(value, dict):
576+
sorted_dict: dict[str, JsonSchemaValue] = {}
577+
keys = value.keys()
578+
if parent_key not in ('properties', 'default'):
579+
keys = sorted(keys)
580+
for key in keys:
581+
sorted_dict[key] = self._sort_recursive(value[key], parent_key=key)
582+
return sorted_dict
583+
elif isinstance(value, list):
584+
sorted_list: list[JsonSchemaValue] = []
585+
for item in value:
586+
sorted_list.append(self._sort_recursive(item, parent_key))
587+
return sorted_list
588+
else:
589+
return value
590+
559591
# ### Schema generation methods
560592
def any_schema(self, schema: core_schema.AnySchema) -> JsonSchemaValue:
561593
"""Generates a JSON schema that matches any value.
@@ -2367,24 +2399,6 @@ def _make_json_hashable(value: JsonValue) -> _HashableJsonValue:
23672399
return value
23682400

23692401

2370-
def _sort_json_schema(value: JsonSchemaValue, parent_key: str | None = None) -> JsonSchemaValue:
2371-
if isinstance(value, dict):
2372-
sorted_dict: dict[str, JsonSchemaValue] = {}
2373-
keys = value.keys()
2374-
if (parent_key != 'properties') and (parent_key != 'default'):
2375-
keys = sorted(keys)
2376-
for key in keys:
2377-
sorted_dict[key] = _sort_json_schema(value[key], parent_key=key)
2378-
return sorted_dict
2379-
elif isinstance(value, list):
2380-
sorted_list: list[JsonSchemaValue] = []
2381-
for item in value: # type: ignore
2382-
sorted_list.append(_sort_json_schema(item, parent_key))
2383-
return sorted_list # type: ignore
2384-
else:
2385-
return value
2386-
2387-
23882402
@dataclasses.dataclass(**_internal_dataclass.slots_true)
23892403
class WithJsonSchema:
23902404
"""Usage docs: https://docs.pydantic.dev/2.10/concepts/json_schema/#withjsonschema-annotation

0 commit comments

Comments
 (0)