Andi
Andi

Reputation: 4899

Generate dynamic model using pydantic

I am trying to create a dynamic model using Python's pydantic library. My input data is a regular dict. However, the content of the dict (read: its keys) may vary.
I am wondering how to dynamically create a pydantic model which is dependent on the dict's content?

I created a toy example with two different dicts (inputs1 and inputs2). Let's assume the nested dict called strategy may be different. Based on strategy/name I know in advance which fields will exist in strategy. I need to create the pydantic model based on strategy/name.

from pydantic import BaseModel

inputs1 = {
    "universe": {"name": "test_universe", "ccy": "USD"},
    "price_src": "csv",
    "strategy": {"name": "test_strat1"},
}
inputs2 = {
    "universe": {"name": "test_universe", "ccy": "USD"},
    "price_src": "csv",
    "strategy": {"name": "test_strat2", "periods": 10},
}


class Universe(BaseModel):
    name: str
    ccy: str = "EUR"


strategy_name = "test_strat2"

if strategy_name == "test_strat1":
    inputs = inputs1

    class Strategy(BaseModel):
        name: str


elif strategy_name == "test_strat2":
    inputs = inputs2

    class Strategy(BaseModel):
        name: str
        periods: int


class StaticModel(BaseModel):
    universe: Universe
    price_src: str = "csv"
    strategy: Strategy


static_model = StaticModel(**inputs)

My expected output if ``strategy_name == "test_strat1":

universe=Universe(name='test_universe', ccy='USD') price_src='csv' strategy=Strategy(name='test_strat1')

My expected output if ``strategy_name == "test_strat2":

universe=Universe(name='test_universe', ccy='USD') price_src='csv' strategy=Strategy(name='test_strat2', periods=10)

I was thinking about using pydantic's create_model function. However, I don't understand how to dynamically define the fields.

Upvotes: 23

Views: 41474

Answers (4)

Alex 222
Alex 222

Reputation: 11

I wanted to create a model for my Fast API POST routes allowed format dynamically to include some fields from other models which I used for my database, consider this example:

from pydantic import BaseModel

class DBModel(BaseModel):
    id: str
    field1: str
    field2: int
    date: datetime = datetime.utcnow()

I only wanted to have field1 and field2 in my POST route data model. Here is what I did (in Pydantic version 2):

from pydantic import create_model

EXCLUDE_FIELDS = ['date', 'id']

view_model_fields = {
    field_name: (field_info.annotation, field_info.default)
    for field_name, field_info in DBModel.model_fields.items()
    if field_name not in EXCLUDE_FIELDS
}

DBModelView = create_model('DBModelView', **view_model_fields)

And then I used it in my route:

@app.post("/my-post-route")
def post_route(data: DBModelView):
    ...
    return data.model_dump()

Pydantic version 1 Equivalent:

from pydantic import create_model

EXCLUDE_FIELDS = ['date', 'id']

DBModelView = create_model('DBModelView')

DBModelView.__fields__ = {
    f: DBModel.__fields__.get(f)
    for f in DBModel.__fields__
    if f not in EXCLUDE_FIELDS
}

Upvotes: 1

pietz
pietz

Reputation: 2553

Syntax has changed a bit in Pydantic 2.x:

d = {"name":(str, ...), "periods":(int, 0)}
Strategy = create_model("Strategy", **d)

You now want to pass tuples of (TYPE, DEFAULT) to the named parameters, or to make it fully dynamic, use a dictionary as shown above.

Upvotes: 0

Khanh Luong
Khanh Luong

Reputation: 542

You can use create_model with key=(type, ...) (3 dots) to declare a field without default value. For example:

from pydantic import BaseModel, create_model

...
if strategy_name == "test_strat1":
    inputs = inputs1
    Strategy = create_model('Strategy', name=(str, ...))

elif strategy_name == "test_strat2":
    inputs = inputs2
    Strategy = create_model('Strategy', name=(str, ...), periods=(int, ...))

print(Strategy.schema_json(indent=2))

Output for test_strat1:

{
  ...
  "properties": {
    "name": {
      "title": "Name",
      "type": "string"
    }
  },
  "required": [
    "name"
  ]
}

And for test_strat2:

{
  ...
  "properties": {
    "name": {
      "title": "Name",
      "type": "string"
    },
    "periods": {
      "title": "Periods",
      "type": "integer"
    }
  },
  "required": [
    "name",
    "periods"
  ]
}

Related Pydantic documentation: https://pydantic-docs.helpmanual.io/usage/models/#dynamic-model-creation

You can see in the docs that:

  • foo=(str, ...) - str-typed foo attribute with no defaults
  • bar=123 - int-typed bar attribute with a default value of 123.

Upvotes: 5

alex_noname
alex_noname

Reputation: 32283

For the dynamic creation of pydantic models, you can use create_model. Like so:

from pydantic import create_model

d = {"strategy": {"name": "test_strat2", "periods": 10}}

Strategy = create_model("Strategy", **d["strategy"])

print(Strategy.schema_json(indent=2))

Output:

{
  "title": "Strategy",
  "type": "object",
  "properties": {
    "name": {
      "title": "Name",
      "default": "test_strat2",
      "type": "string"
    },
    "periods": {
      "title": "Periods",
      "default": 10,
      "type": "integer"
    }
  }
}

Upvotes: 13

Related Questions