Skip to content

test_json_schema - #452

Open
Niccolum wants to merge 3 commits into
reagento:developfrom
Niccolum:test_json_schema
Open

test_json_schema#452
Niccolum wants to merge 3 commits into
reagento:developfrom
Niccolum:test_json_schema

Conversation

@Niccolum

Copy link
Copy Markdown
Contributor

No description provided.

@zhPavel
zhPavel changed the base branch from main to develop July 12, 2026 20:22
Comment on lines +60 to +64
def _format_loader(data):
if isinstance(data, str):
with suppress(ValueError):
return JSONSchemaBuiltinFormat(data)
return data

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why custom loader is required here? Why data for unkown types are directly returned?

@Niccolum Niccolum Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Custom loader needed because format: JSONSchemaBuiltinFormat | str is ambiguous for the default union loader (both sides are string-shaped, so plain str would always win and the enum would never be produced) - enum is tried first, falling back to the raw string for custom formats (see test at test_json_schema_facade.py, test test_load_json_schema_format_uses_builtin_enum_or_falls_back_to_str with "format": "date-time"). Good catch on the fallthrough for non-str data - fixed to raise TypeLoadError.

Comment on lines +18 to +19
def _append_with_patcher(self: S, patcher: Callable[[JSONSchema], JSONSchema]) -> S:
self_copy = copy(self)
self_copy = deepcopy(self)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why copy patcher objects? They should be immutable

@Niccolum Niccolum Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actual issue was list aliasing on _patchers after the shallow copy(). It was same for previous and new item. Use list for fix it, not append

)
from adaptix.load_error import AggregateLoadError

DIALECT_URI = "https://json-schema.org/draft/2020-12/schema"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This constant exists in main library code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! fixed

Comment on lines +48 to +54
def assert_structure(
actual: Union[Mapping[str, Any], list],
expected: Union[Mapping[str, Any], list],
*,
path: str = "",
strict: bool = False,
) -> None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests already have dirty-equals pacakge in dependencies

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

dirty_equals doesn't have exactly what assert_structure needs — no recursive/deep-partial mode, so a nested dict has to be wrapped in its own IsPartialDict at every level, it doesn't happen automatically. But building on top of it still simplified things a lot: assert_structure no longer implements any comparison logic itself, it just builds a tree of IsPartialDict/IsList from the plain dict/list literal and lets dirty_equals do the actual matching.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i remove assert_structure to check all with assert_morphing and, where i can't do it - write raw asserts on keys

Comment on lines +27 to +107
@pytest.mark.parametrize(
["raw", "expected"],
[
("null", JSONSchemaType.NULL),
("boolean", JSONSchemaType.BOOLEAN),
("object", JSONSchemaType.OBJECT),
("array", JSONSchemaType.ARRAY),
("number", JSONSchemaType.NUMBER),
("integer", JSONSchemaType.INTEGER),
("string", JSONSchemaType.STRING),
],
)
def test_load_json_schema_type_enum_values(raw: str, expected: JSONSchemaType):
schema = load_json_schema({"type": raw})

assert schema.type == expected


@pytest.mark.parametrize(
["schema_data", "expected"],
[
(
{"type": "string", "title": "MyStr", "description": "A string"},
JSONSchema(type=JSONSchemaType.STRING, title="MyStr", description="A string"),
),
(
{"type": "string", "format": "date-time"},
JSONSchema(type=JSONSchemaType.STRING, format=JSONSchemaBuiltinFormat.DATE_TIME),
),
(
{"type": "string", "format": "my-custom-format"},
JSONSchema(type=JSONSchemaType.STRING, format="my-custom-format"),
),
(
{
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
},
"required": ["name"],
},
JSONSchema(
type=JSONSchemaType.OBJECT,
properties={
"name": JSONSchema(type=JSONSchemaType.STRING),
"age": JSONSchema(type=JSONSchemaType.INTEGER),
},
required=["name"],
),
),
(
{"anyOf": [{"type": "string"}, {"type": "integer"}]},
JSONSchema(any_of=[
JSONSchema(type=JSONSchemaType.STRING),
JSONSchema(type=JSONSchemaType.INTEGER),
]),
),
],
)
def test_load_json_schema_parses_dict_to_object(schema_data, expected):
schema = load_json_schema(schema_data)

assert schema == expected


def test_load_json_schema_strict_unknown_field_raises():
with pytest.raises(AggregateLoadError):
load_json_schema({"type": "string", "x-custom": "value"}, error_on_extra=True)


def test_load_json_schema_lax_unknown_field_goes_to_extra_keywords():
schema = load_json_schema({"type": "string", "x-custom": "value"}, error_on_extra=False)

assert schema.extra_keywords == {"x-custom": "value"}


def test_load_json_schema_empty_dict():
schema = load_json_schema({})

assert schema == JSONSchema()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are these tests for? They check Adaptix's basic model loading functionality

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point - trimmed the tests that only exercised generic Adaptix dataclass/enum loading, since that's already covered by core Adaptix tests.

Kept only what's actually specific to this facade's retort config in func.py: the custom _format_loader and the error_on_extra behavior (ExtraForbid vs extra_out="extra_keywords") - that logic is unique to load_json_schema and isn't tested elsewhere.

Comment on lines +122 to +145
@pytest.mark.parametrize(
["with_dialect_uri", "expected_schema"],
[
(True, DIALECT_URI),
(False, NOT_EXISTS),
],
ids=["with_uri", "without_uri"],
)
def test_generate_json_schema_dialect_uri(with_dialect_uri: bool, expected_schema: Any): # noqa: FBT001
schema = generate_json_schema(Retort(), SimpleModel, Direction.INPUT, with_dialect_uri=with_dialect_uri)

assert_structure(schema, {"$schema": expected_schema})


def test_generate_json_schema_top_level_keys():
schema = generate_json_schema(Retort(), SimpleModel, Direction.INPUT)

assert_structure(schema, {
"$schema": EXISTS,
"$ref": EXISTS,
"$defs": {
"SimpleModel": EXISTS,
},
}, strict=True)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why these tests checks only keys?

@Niccolum Niccolum Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched this one to assert_morphing. test_generate_json_schema_dialect_uri just check $schema - remove assert_structure from file

Comment on lines +14 to +25
def make_src(tp, schema=None):
if schema is None:
schema = JSONSchema()
return LocalRefSource(value=None, json_schema=schema, loc_stack=LocStack(TypeHintLoc(tp)))


def test_traverse_flat_schema_yields_itself():
schema = JSONSchema(title="flat")

result = list(traverse_json_schema(schema))

assert result == [schema]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why these tests are exist? I do not think that tests for such internal heplers could be useful. This logic shuld be covered via tests of actual user functionality

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remove few of tests from that file

  • test_traverse_flat_schema_yields_itself (traversal returns itself for a schema with no nested fields) -> covered by test_generate_json_schema_matches_full_expected_shape: the name/value fields there are flat schemas, and them showing up correctly in the final properties proves the base case works.

  • test_traverse_ref_to_local_source_visits_referenced_schema (traversal follows into a LocalRefSource) -> covered by test_generate_schemas_namespace_deduplicates_shared_types: Tag is nested in both Article and Post via a real LocalRefSource, and it correctly dedupes into a single $defs entry only if traversal actually follows the ref.

  • test_replace_local_ref_with_prefix_and_ctx (replace_json_schema_ref substitutes $ref from ctx with a prefix) -> covered by test_json_schema_pinned_ref: a custom ref name (ref="MyString") ends up as "$ref": "#/$defs/MyString" in the generated schema, exercising the same ctx→prefix mechanics through the public API.

  • test_replace_nested_properties_recursively_resolved (replace recurses into nested properties) -> covered by the new test_generate_json_schema_prefix_items_dedupes_shared_ref (and any test with a nested dataclass field, e.g. Article.tag): a $ref inside a nested field resolves correctly in the final schema, which requires recursing through properties/prefixItems.

Comment on lines +6 to +17
def apply_patch(patch: JSONSchemaPatch, schema: JSONSchema) -> JSONSchema:
for patcher in patch.get_patchers():
schema = patcher(schema)
return schema


def test_replace_sets_field():
schema = JSONSchema(title="old")

result = apply_patch(JSONSchemaPatch().replace("title", lambda _: "new"), schema)

assert result.title == "new"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Such tests should use high level API (user API) of retorts

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed 3 tests from here:

  • test_replace_sets_field -> test_patch_replace_title (test_json_schema_overrides.py): same .replace("title", ...), same outcome — the new value ends up in the schema, just observed through a real retort instead of manual apply_patch.

  • test_merge_with_chain_first_override_wins -> test_patch_merge_with_chain_first_overrides_inferred: Chain.FIRST overrides the inferred type on price - override wins on a conflicting field, same property being tested.

  • test_merge_with_chain_last_base_wins -> test_patch_merge_with_chain_last_base_wins_over_conflicting_override sets type on both sides - base has the inferred "string", the override supplies "integer" — and asserts the generated schema keeps "string". That conflict resolved by Chain.LAST. With a real conflict on both sides, base wins.

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  src/adaptix/_internal/morphing/facade
  func.py 57, 65
  src/adaptix/_internal/morphing/json_schema
  patch.py
  schema_tools.py
Project Total  

This report was generated by python-coverage-comment-action

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants