Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a blueprint for pydantic 2 #1028

Merged
merged 1 commit into from
Jul 17, 2023

Conversation

caarmen
Copy link
Contributor

@caarmen caarmen commented Jul 17, 2023

Add a blueprint to support pydantic models, using the pydantic 2.x version.

Here's the pydantic 2 migration guide: https://docs.pydantic.dev/latest/migration/

)
auto_schema.registry.register_on_missing(component)

return schema
Copy link
Contributor Author

@caarmen caarmen Jul 17, 2023

Choose a reason for hiding this comment

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

ℹ️ Here's the difference between the two blueprints for pydantic.

The change is to use model_json_schema() instead of model_schema(), which takes a ref_template argument instead of ref_prefix.

Note, I didn't see in the pydantic migration notes anything about migrating from the old model_schema().

$ diff -U5 docs/blueprints/pydantic.py  docs/blueprints/pydantic2.py 
--- docs/blueprints/pydantic.py	2023-07-17 13:38:19.059201949 +0200
+++ docs/blueprints/pydantic2.py	2023-07-17 13:41:14.883652117 +0200
@@ -1,9 +1,9 @@
 from drf_spectacular.extensions import OpenApiSerializerExtension
 from drf_spectacular.plumbing import ResolvedComponent
 
-from pydantic.schema import model_schema
+from pydantic.json_schema import model_json_schema
 
 
 class PydanticExtension(OpenApiSerializerExtension):
     target_class = "pydantic.BaseModel"
     match_subclasses = True
@@ -12,11 +12,11 @@
         return self.target.__name__
 
     def map_serializer(self, auto_schema, direction):
 
         # let pydantic generate a JSON schema
-        schema = model_schema(self.target, ref_prefix="#/components/schemas/")
+        schema = model_json_schema(self.target, ref_template="#/components/schemas")
 
         # pull out potential sub-schemas and put them into component section
         for sub_name, sub_schema in schema.pop("definitions", {}).items():
             component = ResolvedComponent(
                 name=sub_name,

@codecov
Copy link

codecov bot commented Jul 17, 2023

Codecov Report

Patch and project coverage have no change.

Comparison is base (fa568ce) 98.55% compared to head (5a02f01) 98.55%.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1028   +/-   ##
=======================================
  Coverage   98.55%   98.55%           
=======================================
  Files          68       68           
  Lines        8367     8367           
=======================================
  Hits         8246     8246           
  Misses        121      121           

☔ View full report in Codecov by Sentry.
📢 Do you have feedback about the report comment? Let us know in this issue.

@tfranzel tfranzel merged commit b7f79b7 into tfranzel:master Jul 17, 2023
32 checks passed
@tfranzel
Copy link
Owner

awesome! thx @caarmen

@caarmen caarmen deleted the add-pydantic2-blueprint branch July 17, 2023 17:02
@caarmen
Copy link
Contributor Author

caarmen commented Jul 18, 2023

Actually, it seems there's an issue, sorry I didn't catch it earlier :/

For a model with a field which is a list of another pydantic model, the field isn't being generated correctly.

Looks the pydantic 1 model_schema and pydantic 2 model_json_schema aren't exactly equivalent, the subtype information isn't present in model_json_schema.

I'll update here if/when I find more information.

@tfranzel
Copy link
Owner

yes, I also noticed the 2 versions are subtly different. Stuff wasn't just renamed. Feel free to do another PR.

@caarmen
Copy link
Contributor Author

caarmen commented Jul 18, 2023

I hit an issue trying to work around this. I opened an issue on Pydantic: pydantic/pydantic#6741

I haven't managed to generate an openapi json with either #/$defs/ as the location of definitions (which pydantic seems to want), or #/components/schemas (which drf-spectacular seems to want).

Looks like pydantic has an api to be able to put definitions in #/components/schemas, but there's an issue retrieving sub objects when I try to specify this location.

Maybe (hopefully) I just didn't understand the pydantic api enough. 🤞🏻

@caarmen
Copy link
Contributor Author

caarmen commented Jul 18, 2023

I've figured out a workaround, but it's not very clean :/ It basically takes the schema and does a search/replace of #/$defs/ => #/components/schemas/ 😅

Tested in my project:

+import json
+
 from drf_spectacular.extensions import OpenApiSerializerExtension
 from drf_spectacular.plumbing import ResolvedComponent
 from pydantic.json_schema import model_json_schema
 
 
 class PydanticExtension(OpenApiSerializerExtension):
     target_class = "pydantic.BaseModel"
     match_subclasses = True
 
     def get_name(self, auto_schema, direction):
         return self.target.__name__
 
     def map_serializer(self, auto_schema, direction):
 
         # let pydantic generate a JSON schema
-        schema = model_json_schema(self.target, ref_template="#/components/schemas")
+        schema = model_json_schema(self.target)
 
         # pull out potential sub-schemas and put them into component section
-        for sub_name, sub_schema in schema.pop("definitions", {}).items():
+        for sub_name, sub_schema in schema.pop("$defs", {}).items():
             component = ResolvedComponent(
                 name=sub_name,
                 type=ResolvedComponent.SCHEMA,
                 object=sub_name,
                 schema=sub_schema,
             )
             auto_schema.registry.register_on_missing(component)
 
-        return schema
+        # Workaround to force placing definitions in components/schemas.
+        # Pydantic places definitions in $defs, and we want them in components/schemas,
+        # but using ref_template="#/components/schemas in model_json_schema() makes us lose sub objects.
+        # See https://github.com/pydantic/pydantic/issues/6741
+        schema_with_relocated_defs = json.loads(json.dumps(schema).replace("#/$defs/", "#/components/schemas/"))
+        return schema_with_relocated_defs

If you want, I can open a PR for this.

But I understand it's not very clean! 😄

We can wait for some news on the pydantic issue instead.

@caarmen
Copy link
Contributor Author

caarmen commented Jul 18, 2023

scratch that, I can do a proper fix I think 😁
Coming soon (sorry for the noise).

@caarmen
Copy link
Contributor Author

caarmen commented Jul 18, 2023

Here's a PR:

My confidence in this isn't at 100% for now, we could say 😅 . So do feel free to try it out a bit before merging.

I didn't find unit tests for blueprints in the project.

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.

None yet

2 participants