question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

[BUG] Nested Enum Classes With Duplicate Names

See original GitHub issue

Problem It’s a pretty common pattern with Django classes to contain enums/choices classes as a nested class within the model class that uses the enum. If there are multiple nested classes with the same name, even though they are nested under different classes, the generated OpenAPI schema only uses one of them.

Using __qualname__ instead of __name__ should solve this issue for nested classes.

It might be worth looking into namespacing schemas by prefixing them with their module or django app or something, as I imagine this issue occurs with any duplicated names.

Example

class SomeSchema(Schema):
    class Status(str, Enum):
        FOO = "foo"
    status: SomeSchema.Status

class OtherSchema(Schema) 
    class Status(str, Enum):
        BAR= "bar"
    status: OtherSchema.Status

@api.post("/some")
def get_some_thing(request, data: SomeSchema):
    return data.status

@api.post("/other")
def get_other_thing(request, data: OtherSchema):
    return data.status

Only one of the status enums will be present in the resulting schema.

Versions

  • Python version: 3.10.4
  • Django version: 3.2.14
  • Django-Ninja version: 0.19.1

Issue Analytics

  • State:open
  • Created a year ago
  • Comments:6 (3 by maintainers)

github_iconTop GitHub Comments

1reaction
OtherBarrycommented, Dec 5, 2022

@jmduke I ended up just renaming my enums, so instead of Order.Status and Product.Status I have Order.OrderStatus and Product.ProductStatus, which solved the problem for me.

The __qualname__ fix is a pretty easy solution, but might be a big of a pain to monkeypatch. I believe the relevant function is here.

0reactions
jmdukecommented, Dec 6, 2022

@OtherBarry you beat me to the explanation 😃 There’s a couple todos here that I think feint towards the issue, but I couldn’t quite wrap my head around the indirection that goes on in this module. I agree that a better approach for django-ninja to take might be delegate as much of the mapping + conflict resolution as possible to pydantic, which appears to handle it quite well.

To answer your question, though, the monkey-patch in question:

from pydantic import schema

def monkey_patched_get_model_name_map(
    unique_models: schema.TypeModelSet,
) -> dict[schema.TypeModelOrEnum, str]:
    """
    Process a set of models and generate unique names for them to be used as keys in the JSON Schema
    definitions. By default the names are the same as the class name. But if two models in different Python
    modules have the same name (e.g. "users.Model" and "items.Model"), the generated names will be
    based on the Python module path for those conflicting models to prevent name collisions.
    :param unique_models: a Python set of models
    :return: dict mapping models to names
    """
    name_model_map = {}
    conflicting_names: set[str] = set()
    for model in unique_models:
        model_name = schema.normalize_name(model.__qualname__.replace(".", ""))
        if model_name in conflicting_names:
            model_name = schema.get_long_model_name(model)
            name_model_map[model_name] = model
        elif model_name in name_model_map:
            conflicting_names.add(model_name)
            conflicting_model = name_model_map.pop(model_name)
            name_model_map[
                schema.get_long_model_name(conflicting_model)
            ] = conflicting_model
            name_model_map[schema.get_long_model_name(model)] = model
        else:
            name_model_map[model_name] = model
    return {v: k for k, v in name_model_map.items()}


schema.get_model_name_map = monkey_patched_get_model_name_map

The only line here that changes from the original is:

model_name = schema.normalize_name(model.__qualname__.replace(".", ""))

A couple notes:

  • I added the replace so as to prettify the results a bit: I wanted a ref of OrderStatus rather than Order.Status.
  • One potential solution would have been to just call schema.get_long_model_name instead of model.__qualname__. Similarly, I avoided this on aesthetic bounds because long_model_name includes the entire module path which I didn’t want to expose within the ref.
Read more comments on GitHub >

github_iconTop Results From Across the Web

how to overwrite parent class's enum with same name? #1624
Issue description I'm binding a complex c++ library to python, in this lib, it has many inheritance, and for example class A {...
Read more >
c++ - Nested enum and nested class have different behavior
Yes, there are such sections in the C++ Standard. The first one is. 9.9 Nested type names. 1 Type names obey exactly the...
Read more >
191739 – "Duplicate nested type" bogus error on static ... - Bugs
You need to have a JAR file that references the inner class static class/enum, but doesn't contain that class. We have an ANTLR...
Read more >
Nested "enum"s should not be declared static
According to the Java Language Specification-8.9: Nested enum types are implicitly static . So there's no need to declare them static explicitly.
Read more >
Enums - C# language specification - Microsoft Learn
Enums create a set of named constants and are represented by an underlying integral set of ... No two enum members can have...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found