diff --git a/README.md b/README.md index 39dad62..7573a1b 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,7 @@ class Colour(fields.Field): r = "%02X" % (r,) g = "%02X" % (g,) b = "%02X" % (b,) - return '#' + r + g + b + return '#' + r + g + b class Gender(fields.String): def _jsonschema_type_mapping(self): @@ -196,6 +196,44 @@ json_schema = JSONSchema() json_schema.dump(schema) ``` +##### Corner Case: Recursive Custom Types with Custom Containers + +marshmallow-jsonschema supports recursive custom types, and it supports the case where recursion +occurs through a container, such as here where a `Colour` contains a list of its complements + +```python +class Colour(Schema): + complements = fields.List( + fields.Nested("Colour") + ) +``` + +Marshmallow built-in container types like `List` and `Tuple` are supported, as well as +the marshmallow-union extension. *However* if you define a custom container yourself, you'll +need to give your custom container's `_jsonschema_type_mapping` some additional context +to properly recognize the recusive relationship. Here's a contrived example where we used a +custom wrapper to hold complementary colours: + +```python +class Complements(fields.Field): + complements: fields.List + + def __init__(self, field): + self.complements = fields.List(field) + super().__init__() + + # accept extra arguments here so this class can render + # a recursive $ref: #/definitions/Colour + def _jsonschema_type_mapping(self, json_schema, obj): + field_schema = json_schema._get_schema_for_field(obj, self.complements) + return field_schema + +class Colour(Schema): + complements = Complements( + fields.Nested("Colour") + ) +``` + ### React-JSONSchema-Form Extension diff --git a/marshmallow_jsonschema/base.py b/marshmallow_jsonschema/base.py index 700cae4..cfffeb1 100644 --- a/marshmallow_jsonschema/base.py +++ b/marshmallow_jsonschema/base.py @@ -2,7 +2,7 @@ import decimal import uuid from enum import Enum -from inspect import isclass +from inspect import isclass, signature import typing from marshmallow import fields, missing, Schema, validate @@ -254,7 +254,15 @@ def _get_python_type(self, field): def _get_schema_for_field(self, obj, field): """Get schema and validators for field.""" if hasattr(field, "_jsonschema_type_mapping"): - schema = field._jsonschema_type_mapping() + sig = signature(field._jsonschema_type_mapping) + num_args = len(sig.parameters) + if num_args == 2: + # pass down extra context to nested field of + # this schema and obj context if the implementer + # explicitly expects it + schema = field._jsonschema_type_mapping(self, obj) + else: + schema = field._jsonschema_type_mapping() elif "_jsonschema_type_mapping" in field.metadata: schema = field.metadata["_jsonschema_type_mapping"] else: diff --git a/tests/test_dump.py b/tests/test_dump.py index b1d53d5..aa7240b 100644 --- a/tests/test_dump.py +++ b/tests/test_dump.py @@ -744,3 +744,26 @@ class TestSchema(Schema): assert ( len(data["definitions"]["TestSchema"]["properties"]["union_prop"]["anyOf"]) == 3 ) + +def test_recursive_custom_field(): + class NoOpWrapper(fields.Field): + def __init__(self, field): + self.field = field + super().__init__() + + def _jsonschema_type_mapping(self, json_schema, obj): + field_schema = json_schema._get_schema_for_field(obj, self.field) + return field_schema + + class ContrivedSchema(Schema): + recursive = NoOpWrapper( + fields.Nested("ContrivedSchema"), + ) + + schema = ContrivedSchema() + dumped = validate_and_dump(schema) + + assert dumped["definitions"]["ContrivedSchema"]["properties"]["recursive"] == { + "type": "object", + "$ref": "#/definitions/ContrivedSchema" + }