Serializando a instância da classe para JSON

186

Estou tentando criar uma representação de string JSON de uma instância de classe e tendo dificuldades. Digamos que a classe seja construída assim:

class testclass:
    value1 = "a"
    value2 = "b"

Uma chamada para o json.dumps é feita assim:

t = testclass()
json.dumps(t)

Está falhando e me dizendo que a classe de teste não é JSON serializável.

TypeError: <__main__.testclass object at 0x000000000227A400> is not JSON serializable

Eu também tentei usar o módulo pickle:

t = testclass()
print(pickle.dumps(t, pickle.HIGHEST_PROTOCOL))

E fornece informações da instância da classe, mas não um conteúdo serializado da instância da classe.

b'\x80\x03c__main__\ntestclass\nq\x00)\x81q\x01}q\x02b.'

O que estou fazendo de errado?

Ferhan
fonte
30
Use uma linha, s = json.dumps(obj, default=lambda x: x.__dict__)para as variáveis de instância serialize do objeto ( self.value1, self.value2...). É a maneira mais simples e direta. Serializará estruturas de objetos aninhados. A defaultfunção é chamada quando um determinado objeto não é diretamente serializável. Você também pode olhar para a minha resposta abaixo. Achei as respostas populares desnecessariamente complexas, que provavelmente eram verdadeiras há muito tempo.
codeman48
1
Como você testclassnão possui __init__()método, todas as instâncias compartilharão os mesmos dois atributos de classe ( value1e value2) definidos na instrução de classe. Você entende a diferença entre uma classe e uma instância de uma?
martineau
1
Há uma biblioteca python para este github.com/jsonpickle/jsonpickle (comentando desde resposta é muito abaixo na linha e não vai ser acessível.)
os melhores desejos

Respostas:

238

O problema básico é que o codificador JSON json.dumps()sabe apenas como serializar um conjunto limitado de tipos de objetos por padrão, todos os tipos internos. Listar aqui: https://docs.python.org/3.3/library/json.html#encoders-and-decoders

Uma boa solução seria fazer sua classe herdar JSONEncodere, em seguida, implementar a JSONEncoder.default()função, e fazer com que essa função emita o JSON correto para sua classe.

Uma solução simples seria chamar json.dumps()o .__dict__membro dessa instância. Esse é um Python padrão dicte, se sua classe for simples, será JSON serializável.

class Foo(object):
    def __init__(self):
        self.x = 1
        self.y = 2

foo = Foo()
s = json.dumps(foo) # raises TypeError with "is not JSON serializable"

s = json.dumps(foo.__dict__) # s set to: {"x":1, "y":2}

A abordagem acima é discutida nesta postagem do blog:

    Serializando objetos Python arbitrários para JSON usando __dict__

steveha
fonte
3
Eu tentei isso. O resultado final de uma chamada para json.dumps (t .__ dict__) é apenas {}.
Ferhan
6
Isso ocorre porque sua classe não possui uma .__init__()função de método, portanto, sua instância de classe possui um dicionário vazio. Em outras palavras, {}é o resultado correto para o seu código de exemplo.
Steveha
3
Obrigado. Isso faz o truque. Eu adicionei um init simples sem parâmetros e agora, chamando o json.dumps (t .__ dict__) retorna os dados adequados no formato de: {"value2": "345", "value1": "123"} eu já tinha visto posts como isso antes, não tinha certeza se eu precisava de um serializador personalizado para os membros, a necessidade de init não foi mencionada explicitamente ou eu a perdi. Obrigado.
Ferhan
3
Este trabalho para uma única classe, mas não com objets classes relacionadas
Nwawel A Iroume
2
@NwawelAIroume: True. Se você tem um objeto que por exemplo, está contendo vários objetos em uma lista o erro ainda éis not JSON serializable
gies0r
57

Há uma maneira que funciona muito bem para mim que você pode experimentar:

json.dumps()pode usar um parâmetro opcional padrão, onde você pode especificar uma função serializadora personalizada para tipos desconhecidos, que no meu caso se parece com

def serialize(obj):
    """JSON serializer for objects not serializable by default json code"""

    if isinstance(obj, date):
        serial = obj.isoformat()
        return serial

    if isinstance(obj, time):
        serial = obj.isoformat()
        return serial

    return obj.__dict__

Os dois primeiros ifs são para serialização de data e hora e, em seguida, é obj.__dict__retornado para qualquer outro objeto.

a chamada final se parece com:

json.dumps(myObj, default=serialize)

É especialmente bom quando você está serializando uma coleção e não deseja chamar __dict__explicitamente para cada objeto. Aqui é feito automaticamente para você.

Até agora funcionou tão bem para mim, ansioso por seus pensamentos.

Brócolis
fonte
Eu entendo NameError: name 'serialize' is not defined. Alguma dica?
Kyle Delaney
Muito agradável. Apenas para classes que têm slots:try: dict = obj.__dict__ except AttributeError: dict = {s: getattr(obj, s) for s in obj.__slots__ if hasattr(obj, s)} return dict
fantastory 28/06/19
É incrível que uma linguagem tão popular não tenha um liner para jsoniny um objeto. Deve ser porque não está estaticamente digitado.
TheRennen
49

Você pode especificar o defaultparâmetro nomeado na json.dumps()função:

json.dumps(obj, default=lambda x: x.__dict__)

Explicação:

Forme os documentos ( 2.7 , 3.6 ):

``default(obj)`` is a function that should return a serializable version
of obj or raise TypeError. The default simply raises TypeError.

(Funciona em Python 2.7 e Python 3.x)

Nota: Nesse caso, você precisa de instancevariáveis ​​e não de classvariáveis, como o exemplo da pergunta tenta fazer. (Estou supondo que o autor da pergunta class instanceseja um objeto de uma classe)

Eu aprendi isso primeiro com a resposta de @ phihag aqui . Considerou a maneira mais simples e limpa de fazer o trabalho.

codeman48
fonte
6
Isso funcionou para mim, mas por causa de membros datetime.date eu mudei um pouco:default=lambda x: getattr(x, '__dict__', str(x))
Dakota Hawkins
@Dakota boa solução alternativa; datetime.dateé uma implementação C, portanto, não possui nenhum __dict__atributo. IMHO em nome da uniformidade, datetime.datedeve ser tê-lo ...
codeman48
22

Eu só faço:

data=json.dumps(myobject.__dict__)

Esta não é a resposta completa e, se você tiver algum tipo de classe de objeto complicada, certamente não receberá tudo. No entanto, eu uso isso para alguns dos meus objetos simples.

Uma que funciona muito bem é a classe "options" que você obtém do módulo OptionParser. Aqui está junto com a própria solicitação JSON.

  def executeJson(self, url, options):
        data=json.dumps(options.__dict__)
        if options.verbose:
            print data
        headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
        return requests.post(url, data, headers=headers)
SpiRail
fonte
Você pode querer se remover, se não estiver usando isso dentro de uma classe.
SpiRail
3
Isso funcionará bem, desde que o objeto não seja composto de outros objetos.
Haroldo_OK
19

Usando jsonpickle

import jsonpickle

object = YourClass()
json_object = jsonpickle.encode(object)
gies0r
fonte
5

JSON não é realmente destinado a serializar objetos Python arbitrários. É ótimo para serializar dictobjetos, mas o picklemódulo é realmente o que você deve usar em geral. A saída de picklenão é realmente legível por humanos, mas deve ser bem feita. Se você insistir em usar JSON, poderá verificar ojsonpickle módulo, que é uma abordagem híbrida interessante.

https://github.com/jsonpickle/jsonpickle

Brendan Wood
fonte
9
O principal problema que vejo com pickle é que é um formato específico para Python, enquanto JSON é um formato independente de plataforma. O JSON é especialmente útil se você estiver escrevendo um aplicativo da Web ou um back-end para algum aplicativo móvel. Dito isto, obrigado por apontar para jsonpickle.
Haroldo_OK 27/05
@Haroldo_OK O jsonpickle ainda não exporta para JSON, apenas não é muito legível por humanos?
Caelum
4

Aqui estão duas funções simples para serialização de qualquer classe não sofisticada, nada sofisticado, como explicado anteriormente.

Eu uso isso para coisas do tipo de configuração porque posso adicionar novos membros às classes sem nenhum ajuste de código.

import json

class SimpleClass:
    def __init__(self, a=None, b=None, c=None):
        self.a = a
        self.b = b
        self.c = c

def serialize_json(instance=None, path=None):
    dt = {}
    dt.update(vars(instance))

    with open(path, "w") as file:
        json.dump(dt, file)

def deserialize_json(cls=None, path=None):
    def read_json(_path):
        with open(_path, "r") as file:
            return json.load(file)

    data = read_json(path)

    instance = object.__new__(cls)

    for key, value in data.items():
        setattr(instance, key, value)

    return instance

# Usage: Create class and serialize under Windows file system.
write_settings = SimpleClass(a=1, b=2, c=3)
serialize_json(write_settings, r"c:\temp\test.json")

# Read back and rehydrate.
read_settings = deserialize_json(SimpleClass, r"c:\temp\test.json")

# results are the same.
print(vars(write_settings))
print(vars(read_settings))

# output:
# {'c': 3, 'b': 2, 'a': 1}
# {'c': 3, 'b': 2, 'a': 1}
GBGOLC
fonte
3

Existem algumas boas respostas sobre como começar a fazer isso. Mas há algumas coisas a serem lembradas:

  • E se a instância estiver aninhada dentro de uma grande estrutura de dados?
  • E se também quiser o nome da classe?
  • E se você quiser desserializar a instância?
  • E se você estiver usando em __slots__vez de __dict__?
  • E se você simplesmente não quiser fazer isso sozinho?

O json-tricks é uma biblioteca (que eu criei e outros contribuímos) que foi capaz de fazer isso por um bom tempo. Por exemplo:

class MyTestCls:
    def __init__(self, **kwargs):
        for k, v in kwargs.items():
            setattr(self, k, v)

cls_instance = MyTestCls(s='ub', dct={'7': 7})

json = dumps(cls_instance, indent=4)
instance = loads(json)

Você recuperará sua instância. Aqui o json fica assim:

{
    "__instance_type__": [
        "json_tricks.test_class",
        "MyTestCls"
    ],
    "attributes": {
        "s": "ub",
        "dct": {
            "7": 7
        }
    }
}

Se você gosta de criar sua própria solução, pode procurar na fonte json-trickspara não esquecer alguns casos especiais (como __slots__).

Ele também faz outros tipos, como matrizes numpy, datetime, números complexos; também permite comentários.

Marca
fonte
3

Python3.x

A melhor abordagem que pude alcançar com meu conhecimento foi essa.
Observe que esse código também trata set ().
Essa abordagem é genérica, apenas necessitando da extensão da classe (no segundo exemplo).
Observe que estou fazendo isso apenas com arquivos, mas é fácil modificar o comportamento ao seu gosto.

No entanto, este é um CoDec.

Com um pouco mais de trabalho, você pode construir sua classe de outras maneiras. Eu assumo um construtor padrão para instância, então eu atualizo o dict de classe.

import json
import collections


class JsonClassSerializable(json.JSONEncoder):

    REGISTERED_CLASS = {}

    def register(ctype):
        JsonClassSerializable.REGISTERED_CLASS[ctype.__name__] = ctype

    def default(self, obj):
        if isinstance(obj, collections.Set):
            return dict(_set_object=list(obj))
        if isinstance(obj, JsonClassSerializable):
            jclass = {}
            jclass["name"] = type(obj).__name__
            jclass["dict"] = obj.__dict__
            return dict(_class_object=jclass)
        else:
            return json.JSONEncoder.default(self, obj)

    def json_to_class(self, dct):
        if '_set_object' in dct:
            return set(dct['_set_object'])
        elif '_class_object' in dct:
            cclass = dct['_class_object']
            cclass_name = cclass["name"]
            if cclass_name not in self.REGISTERED_CLASS:
                raise RuntimeError(
                    "Class {} not registered in JSON Parser"
                    .format(cclass["name"])
                )
            instance = self.REGISTERED_CLASS[cclass_name]()
            instance.__dict__ = cclass["dict"]
            return instance
        return dct

    def encode_(self, file):
        with open(file, 'w') as outfile:
            json.dump(
                self.__dict__, outfile,
                cls=JsonClassSerializable,
                indent=4,
                sort_keys=True
            )

    def decode_(self, file):
        try:
            with open(file, 'r') as infile:
                self.__dict__ = json.load(
                    infile,
                    object_hook=self.json_to_class
                )
        except FileNotFoundError:
            print("Persistence load failed "
                  "'{}' do not exists".format(file)
                  )


class C(JsonClassSerializable):

    def __init__(self):
        self.mill = "s"


JsonClassSerializable.register(C)


class B(JsonClassSerializable):

    def __init__(self):
        self.a = 1230
        self.c = C()


JsonClassSerializable.register(B)


class A(JsonClassSerializable):

    def __init__(self):
        self.a = 1
        self.b = {1, 2}
        self.c = B()

JsonClassSerializable.register(A)

A().encode_("test")
b = A()
b.decode_("test")
print(b.a)
print(b.b)
print(b.c.a)

Editar

Com um pouco mais de pesquisa, encontrei uma maneira de generalizar sem a necessidade da chamada do método de registro SUPERCLASS , usando uma metaclasse

import json
import collections

REGISTERED_CLASS = {}

class MetaSerializable(type):

    def __call__(cls, *args, **kwargs):
        if cls.__name__ not in REGISTERED_CLASS:
            REGISTERED_CLASS[cls.__name__] = cls
        return super(MetaSerializable, cls).__call__(*args, **kwargs)


class JsonClassSerializable(json.JSONEncoder, metaclass=MetaSerializable):

    def default(self, obj):
        if isinstance(obj, collections.Set):
            return dict(_set_object=list(obj))
        if isinstance(obj, JsonClassSerializable):
            jclass = {}
            jclass["name"] = type(obj).__name__
            jclass["dict"] = obj.__dict__
            return dict(_class_object=jclass)
        else:
            return json.JSONEncoder.default(self, obj)

    def json_to_class(self, dct):
        if '_set_object' in dct:
            return set(dct['_set_object'])
        elif '_class_object' in dct:
            cclass = dct['_class_object']
            cclass_name = cclass["name"]
            if cclass_name not in REGISTERED_CLASS:
                raise RuntimeError(
                    "Class {} not registered in JSON Parser"
                    .format(cclass["name"])
                )
            instance = REGISTERED_CLASS[cclass_name]()
            instance.__dict__ = cclass["dict"]
            return instance
        return dct

    def encode_(self, file):
        with open(file, 'w') as outfile:
            json.dump(
                self.__dict__, outfile,
                cls=JsonClassSerializable,
                indent=4,
                sort_keys=True
            )

    def decode_(self, file):
        try:
            with open(file, 'r') as infile:
                self.__dict__ = json.load(
                    infile,
                    object_hook=self.json_to_class
                )
        except FileNotFoundError:
            print("Persistence load failed "
                  "'{}' do not exists".format(file)
                  )


class C(JsonClassSerializable):

    def __init__(self):
        self.mill = "s"


class B(JsonClassSerializable):

    def __init__(self):
        self.a = 1230
        self.c = C()


class A(JsonClassSerializable):

    def __init__(self):
        self.a = 1
        self.b = {1, 2}
        self.c = B()


A().encode_("test")
b = A()
b.decode_("test")
print(b.a)
# 1
print(b.b)
# {1, 2}
print(b.c.a)
# 1230
print(b.c.c.mill)
# s
Davi Abreu Wasserberg
fonte
2

Acredito que, em vez de herança, como sugerido na resposta aceita, é melhor usar o polimorfismo. Caso contrário, você precisará ter uma grande declaração if else para personalizar a codificação de cada objeto. Isso significa criar um codificador padrão genérico para JSON como:

def jsonDefEncoder(obj):
   if hasattr(obj, 'jsonEnc'):
      return obj.jsonEnc()
   else: #some default behavior
      return obj.__dict__

e, em seguida, tenha uma jsonEnc()função em cada classe que você deseja serializar. por exemplo

class A(object):
   def __init__(self,lengthInFeet):
      self.lengthInFeet=lengthInFeet
   def jsonEnc(self):
      return {'lengthInMeters': lengthInFeet * 0.3 } # each foot is 0.3 meter

Então você liga json.dumps(classInstance,default=jsonDefEncoder)

hwat
fonte