Pydantic: ключи словаря кодирования JSON

#python #json #pydantic

Вопрос:

При использовании (хэшируемых) объектов в качестве ключей словаря вызов .json() завершается неудачно, поскольку, хотя значения закодированы, ключи не:

 from pydantic import BaseModel
from typing import dict
from datetime import datetime

class Foo(BaseModel): 
  date: datetime 
  sdict: Dict[datetime, str]

  class Config:
    json_encoders = {
      datetime: repr
    }

foo = Foo(date=datetime.now(), sdict={datetime.now(): 'now'})
foo                                                                                                                                                                                                      
# Foo(date=datetime.datetime(2021, 9, 3, 12, 9, 55, 36105), sdict={datetime.datetime(2021, 9, 3, 12, 9, 55, 36114): 'now'})
foo.json()
TypeError: keys must be a string

# to prove the other way around works:
class Foo(BaseModel): 
  date: datetime 
  sdict: Dict[str, datetime]

  class Config:
    json_encoders = {
      datetime: repr
    }

foo = Foo(date=datetime.now(), sdict={'now': datetime.now()})
foo.json()                                                                                                                                                                                               
# '{"date": "datetime.datetime(2021, 9, 3, 12, 13, 30, 606880)", "sdict": {"now": "datetime.datetime(2021, 9, 3, 12, 13, 30, 606884)"}}'

 

Это связано с тем, что default= параметр, в json.dumps() котором в конечном итоге используется для сброса, не кодирует ключи словаря. Определение класса кодировщика JSON действительно работает, но для меня это не работает по другим причинам.

Я видел TypedDict в pydantic, но, похоже, это не решает проблему. На самом деле, я не уверен , в чем смысл TypedDict , так как AFAICS вам нужно определить каждый ключ в dict, что делает его аналогом статического объекта?

Мой вариант использования заключается в том, что мне нужно представить следующую идею:

 {
  "report": {
    "warehouses": {
      warehouse.id: {
        "name": warehouse.name,
        "address": warehouse.address,
      }
      for warehouse in warehouses
  }
}
 

и warehouse.id является Identifier объектом, который может быть преобразован в различные форматы по требованию, и который кодировщик json преобразует в строку.

Кто-нибудь знает способ, отличный от словаря, где я могу добавлять произвольные ключи к объекту таким образом, чтобы это зависело от кодировщика json, или каким-либо другим способом сериализации?

Ответ №1:

Одним из вариантов решения проблемы является использование пользовательской json_dumps функции для модели pydantic, внутри которой для выполнения пользовательской сериализации я сделал это, унаследовав от JSONEncoder .

Например, вот так:

 import json
from pydantic import BaseModel
from typing import Dict
from datetime import datetime


class CustomEncoder(json.JSONEncoder):
    def _transform(self, v):
        res = v
        if isinstance(v, datetime):
            res = v.isoformat()
        # else other variants
        return self._encode(res)

    def _encode(self, obj):
        if isinstance(obj, dict):
            return {self._transform(k): self._transform(v) for k, v in obj.items()}
        else:
            return obj

    def encode(self, obj):
        return super(CustomEncoder, self).encode(self._encode(obj))


def custom_dumps(values, *, default):
    return CustomEncoder().encode(values)


class Foo(BaseModel):
    date: datetime
    sdict: Dict[datetime, str]

    class Config:
        json_dumps = custom_dumps


foo = Foo(date=datetime.now(), sdict={datetime.now(): 'now'})
Foo(date=datetime(2021, 9, 3, 12, 9, 55, 36105), sdict={datetime(2021, 9, 3, 12, 9, 55, 36114): 'now'})
print(foo.json())
 
 {"date": "2021-09-07T16:02:51.070159", "sdict": {"2021-09-07T16:02:51.070164": "now"}}