datamodel-code-generator icon indicating copy to clipboard operation
datamodel-code-generator copied to clipboard

JSON Schema: oneOf with const

Open roquie opened this issue 1 year ago • 1 comments

Describe the bug Does not generate an enumeration for oneOf + cost. For example, autocomplete in Jetbrains IDE works fine for this case.

To Reproduce

  1. create a schema
  2. generate
  3. get an incorrect result generation

Example schema:

# definition

  nodeJsModeEnum:
    title: NodeJS mode
    type: string
    description: |
       A long description here.
    default: npm
    oneOf:
      - title: npm
        const: npm
      - title: yarn
        const: yarn
      - title: npm ci
        const: npm_ci

# usage
        properties:
          mode:
            $ref: "#/definitions/nodejsModeEnum"

Used commandline:

pdm run datamodel-codegen --input config/schema/cd.schema.yaml --input-file-type jsonschema --output src/config/cd_model.py --output-model-type pydantic_v2.BaseModel

# pyproject.toml options
#[tool.datamodel-codegen]
#field-constraints = true
#snake-case-field = true
#strip-default-none = false
#target-python-version = "3.11"

Expected behavior

# expects
class NodeJsModeEnum(Enum):
    npm = 'npm'
    yarn = 'yarn'
    npm_ci = 'npm_ci'

# actual
class NodeJsModeEnum(RootModel[str]):
    root: str = Field(
        ..., description='...'
    )

Version:

  • OS: MacOS
  • Python version: 3.11
  • datamodel-code-generator version: 0.25.5

roquie avatar Apr 17 '24 18:04 roquie

How to fix with problem ? @roquie

LisicynV avatar Sep 26 '24 11:09 LisicynV

Would like to add on that - having enums defined that way are much more useful that using the plain "enum" property. That's because they allow attaching description for each enum member!

As a workaround

You can define the "enum" property as well as "oneOf" and it will generate code correctly and maintain the description of each enum member.

Example:

Given the JSON Schema:

{
    "$schema": "https://json-schema.org/draft-07/schema#",
    "type": "object",
    "required": ["difficulty_level"],
    "properties": {
        "difficulty_level": {
            "title": "The difficulty level of the game",
            "enum": ["easy", "medium", "hard"],
            "oneOf": [
                {
                    "const": "easy",
                    "description": "Soft intro to game"
                },
                {
                    "const": "medium",
                    "description": "To moderate players"
                },
                {
                    "const": "hard",
                    "description": "To really advanced players who looking for challenge"
                }
            ]
        }
    }
}

Note that it's important to distinguish between "title" property and "description" which work together to provide documentation for JSON file editors:

Image

The generated code will be:

# generated by datamodel-codegen:
#   filename:  example-json-schema.json
#   timestamp: 2025-06-30T08:50:14+00:00

from __future__ import annotations

from enum import Enum

from pydantic import BaseModel, Field


class DifficultyLevel(Enum):
    easy = 'easy'
    medium = 'medium'
    hard = 'hard'


class Model(BaseModel):
    difficulty_level: DifficultyLevel = Field(
        ..., title='The difficulty level of the game'
    )

This achieves what you would expect - but without documentation on each enum member. Those can be added manually:

# ...

from enum import Enum

class DifficultyLevel(Enum):
    easy = "easy"
    """Soft intro to game"""
    medium = "medium"
    """To moderate players"""
    hard = "hard"
    """To really advanced players who looking for challenge"""


# ....

ExcellentAmericanEagle avatar Jun 30 '25 08:06 ExcellentAmericanEagle

There's also a useful use case where each enum member in the "oneof" receives a "title" or "description". Because you can't annotate enum members nicely like the following. Because it raises the error:

Type annotations are not allowed for enum members Pylance - [reportGeneralTypeIssues](https://github.com/microsoft/pylance-release/blob/main/docs/diagnostics/reportGeneralTypeIssues.md)

Maybe we can take a different approach for that:

Given the JSON Schema:

{
    "$schema": "https://json-schema.org/draft-07/schema#",
    "type": "object",
    "required": ["difficulty_level"],
    "properties": {
        "difficulty_level": {
            "title": "The difficulty level of the game",
            "oneOf": [
                {
                    "const": "easy",
                    "title": "Easy",
                    "description": "Perfect for beginners and newcomers"
                },
                {
                    "const": "medium",
                    "title": "Medium",
                    "description": "More challenging, but better rewards"
                },
                {
                    "const": "hard",
                    "title": "Hard",
                    "description": "The most rewarding, yet the most challenging"
                }
            ]
        }
    }
}

The generated code would be:

# Notice `StrEnum` is used instead of plain `Enum`
class DifficultyLevel(StrEnum):
    def __init__(self, _, title, description) -> None:
        self.title = title
        self.description = description

    def __new__(cls, value, title, description):
        member = str.__new__(cls, value)
        member._value_ = value
        member.title = title
        member.description = description
        return member

    EASY = "easy", "Easy", "Perfect for beginners and newcomers"
    MEDIUM = "medium", "Medium", "More challenging, but better rewards"
    HARD = "hard", "Hard", "The most rewarding yet the most challenging"


difficulty_level: DifficultyLevel = DifficultyLevel.EASY

# This gives so much flexibility!
print(difficulty_level.title)
print(difficulty_level.description)

ExcellentAmericanEagle avatar Jul 14 '25 15:07 ExcellentAmericanEagle