Oj: Renderizar JSON com ActiveModel :: Serializers retorna o objeto

Criado em 28 out. 2014  ·  55Comentários  ·  Fonte: ohler55/oj

Fiz uma atualização do _oj_ da versão 2.10.2 para 2.10.4 e recebi um objeto serializado em vez do JSON esperado.

{
    "object": {
        "klass": "Client",
        "table": {
            "name": "clients",
            "engine": "Client",
            "columns": null,
            "aliases": [],
            "table_alias": null,
            "primary_key": null
        },
        "values": {
            "references": [],
            "where": [
                {
                    "left": "#<struct Arel::Attributes::Attribute relation=#<Arel::Table:0x007fdeb9db0b10 @name=\"clients\", @engine=Client(id: uuid, email: string, phone: string, full_name: string, created_at: datetime, updated_at: datetime, service_provider_id: uuid, country_code: string, lang: string), @columns=nil, @aliases=[], @table_alias=nil, @primary_key=nil>, name=\"service_provider_id\">",
                    "right": "3ba32f03-0ce0-439d-a26a-8a6617d92116"
                }
            ],
            ...
}

Eu uso _rails-api 0.3.1_ e _active_model_serializers 0.9.0_

Comentários muito úteis

Olá!

A situação em que nos metemos é bastante lamentável, pois temos várias bibliotecas JSON brigando entre si sobre nomes de métodos muito genéricos como to_json : decepcionado:

Resumo Rápido

  1. Os desenvolvedores Rails devem sempre substituir #as_json vez de #to_json
  2. Rails espera que a chamada de #to_json sempre passe pelo codificador Rails (codificador Ruby puro personalizado para <= 4.0, json gem para 4.1+), que considera os ganchos #as_json entre outras coisas
  3. Para usar Oj explicitamente, os desenvolvedores devem chamar Oj.dump(obj)
  4. Para usar json gem explicitamente, os desenvolvedores devem chamar JSON.generate(obj) (isso só funciona de forma confiável no Rails 4.1+)
  5. A definição #to_json simplesmente chame rails_json_encoder_encode(self.as_json) em todas as versões (ligeiramente simplificado)
  6. Oj pode / deve invocar #as_json em objetos que não sabe codificar nativamente

Antes de entrarmos em detalhes, aqui estão algumas histórias de como chegamos à situação atual.

Codificador Rails JSON

História Antiga (era Rails 2.3)

Era uma vez, Ruby não vinha com nenhuma biblioteca JSON. Como o Rails precisava gerar objetos JSON, escrevemos nosso próprio codificador e, eventualmente, decidimos pela API baseada em to_json em suas classes, constrói um hash para representar seus dados e chama recursivamente to_json nesse Hash, e você tem a string JSON desejada. (Curiosidade: esse commit também incluiu o decodificador JSON inicial que funcionou essencialmente convertendo JSON em YAML usando gsub para que pudesse ser analisado com Syck. Tempos divertidos!)

Rails 3 era

Em algum ponto, os Rubistas começaram a escrever outras bibliotecas / wrappers JSON que são mais eficientes do que o codificador Ruby puro que vem com Rails, como Oj , yajl , etc. Porém, há um problema - a API to_json não oferece nenhuma maneira de cooperarmos com essas bibliotecas. Como to_json é responsável por retornar a string JSON resultante, não há como esses codificadores alternativos "se conectarem" a esse processo.

A observação principal aqui é que a maioria dos programadores de aplicativos não substituem to_json porque desejam controlar como os objetos Ruby são _encodidos_ em uma string JSON; eles simplesmente desejam descrever como desejam seus dados _representados_ (por exemplo, quais campos incluir, se o objeto deve ser mapeado para um Array ou um Objeto ...).

Assim, as_json nasceu . A ideia é que os desenvolvedores podem simplesmente substituir as_json para retornar a representação Ruby de seus dados (ou seja, retornar um Hash / Array / etc) e deixar que o codificador se encarregue de transformá-lo em uma string JSON.

Por motivos de compatibilidade com versões anteriores, o método #to_json é mantido como um "ponto de entrada" para nosso codificador baseado em Ruby (1.8.7 não tem um codificador JSON na biblioteca padrão). No entanto, agora deve ser possível usar um codificador alternativo, como Oj, por exemplo, chamando Oj.dump(some_rails_object_that_oj_doesnt_know_about) . Nesse caso, o codificador oj pode simplesmente invocar #as_json no objeto para obter uma representação simplificada (esperançosamente *) do objeto que será capaz de codificar.

(* o gancho as_json é fornecido como uma "dica" para o codificador - o desenvolvedor é livre para retornar quaisquer objetos estranhos no gancho e cabe ao codificador decidir o que fazer com eles. Na prática no entanto, o significado de coisas como símbolos, hashes, matrizes, booleanos e strings são bem compreendidos. Isso não precisa ser recorrente, portanto, se o desenvolvedor retornou, digamos, uma matriz de não literais, o codificador ainda deve tentar chamar as_json em seus elementos. Fiz algumas anotações detalhadas sobre como um codificador razoável _deve_ se comportar aqui.)

Rails 4 era

Quando o Ruby 1.9.1 foi lançado, a gema json tornou-se oficialmente parte da biblioteca padrão do Ruby. Desde que o 4.0 abandonou o suporte para Ruby 1.8.7, em teoria tudo deve estar bem a partir de agora. Infelizmente, ele usa a arquitetura #to_json , o que o impedia de ser muito útil dentro do Rails por causa das falhas descritas acima. Assim, as coisas permaneceram praticamente inalteradas.

À medida que a popularidade da gema JSON crescia, outro problema irritante surgiu. Como mencionado acima, a gema json também define um método to_json que não é exatamente o mesmo que vem com o Rails. (por exemplo, um pega um hash de opção, um espera um objeto State ; um considera as_json , o outro não). Isso criou todos os tipos de problemas e confusão .

Rails 4.1+

Peguei a tarefa de limpar algumas dessas bagunças. Algumas coisas mudaram:

  • Anteriormente, Rails e json gem pareciam se dar bem um com o outro até que as coisas explodissem repentinamente em alguns casos extremos. O principal problema é que o JSON gem espera e passa um objeto hash-like-mas-não-realmente State como o segundo argumento para to_json , onde Rails espera um hash real. Para evitar esses problemas, detectamos e contornamos completamente o codificador Rails quando a gema JSON está envolvida. Então, quando você chama ActiveSupport::JSON.encode(obj) ou obj.to_json , você obtém as coisas do Rails, enquanto quando você chama explicitamente JSON.{generate|dump}(obj) você obtém o resultado "básico" do JSON gem sem nenhum dos Rails coisas ( as_json ).
  • Como o Ruby 1.9+ vem com uma gema json , não há mais sentido em manter o codificador Ruby puro dentro do Rails. Eu arranquei todas as coisas relacionadas à codificação dentro do Rails (ou seja, o código que converte o objeto Ruby em strings JSON reais ... por exemplo, nil => "null" ), e simplesmente deslizei a joia JSON . Então, quando você chama to_json , o Rails primeiro chama recursivamente as_json no objeto e passa isso para JSON.generate .
  • Uma vez que os mantenedores do multijson pareciam desinteressados ​​em continuar a manter aquela gema, ela foi retirada do Rails em favor de usar apenas a gema embutida json . Isso afeta apenas a análise! Nas versões anteriores, o Rails usava apenas multi-json no lado da análise. O lado da codificação sempre passou pelo próprio codificador do Rails (por meio de #to_json ) com ou sem multi-json / oj / json gem ativado. Exceto pelos pontos que você usou explicitamente MultiJson.dump(...) , você sempre usou o codificador interno do Rails. Instalar o Oj gem não diz ao Rails para usar o codificador Oj independente das configurações do MultiJson. Qualquer efeito na velocidade de _encoding_ que você notou é provavelmente psicológico :)

Controlador Rails

Quando você chama render json: some_object , o Rails chama #to_json no objeto, que deve passar pelo codificador Rails pronto para usar.

Serializador de modelo ativo

v0.8 e 0.9

Essas duas versões do AM :: S têm uma base de código completamente diferente, mas para o nosso propósito são virtualmente iguais. Quando você faz um render json: object , ele tenta envolver object no objeto serializador apropriado (que implementa as_json ) e passar esse objeto (ainda um objeto Ruby neste ponto) para o renderizador json do controlador, que chama to_json conforme descrito acima.

Se você estiver usando o Rails 4.2, você precisará do 0.8.2 / 0.9.0 para que as coisas funcionem corretamente porque o Rails renomeou alguns dos métodos de gancho do controlador.

v0.10

Esta é outra reescrita completa, mas atualmente define to_json vez de as_json .

( @guilleiguaran precisamos: scissors: estes to_json em AM :: S em favor de as_json como indiquei aqui )

Onde as coisas deram errado com Oj + Rails

A base de código do Rails depende de to_json todas as áreas. Por padrão, isso deve resolver para o codificador JSON do Rails, que usa o gancho as_json . Parece que em algum lugar da gema Oj ele substitui to_json por uma definição que ignora os ganchos as_json (provavelmente aqui ?).

Onde isso nos deixa ...

Opção 1: ativar explicitamente

  1. Configure Oj para ...

    1. Não substituir to_json

    2. Honra as_json

    3. (Acredito que o acima equivale a "não use o modo json gem imitar e habilite o modo compat", mas posso estar errado)

  2. Use explicitamente render json: Oj.dump(obj) e render json: Oj.dump(WhateverSerializer.new(obj)) em seus controladores

É basicamente assim que as coisas sempre funcionaram com as versões anteriores do Oj. (Novamente, o MultiJson nunca o ajudou na codificação.)

Presumivelmente, você pode fazer o monkey patch no renderizador json para fazer isso automaticamente para você (nesse caso, as adições do renderizador AM :: S também devem funcionar ™).

Opção 2: usar Oj automaticamente para tudo ...

Isso requer patch Oj para oferecer um modo que sobrescreveria to_json para codificar objetos de uma maneira compatível com Rails . Isso é super invasivo e você provavelmente acabará lidando com muitos dos mesmos bugs com os quais lidamos antes, então não sei se eu poderia recomendar isso.

Além disso, é bem provável que desviemos um pouco no resultado da codificação, mas se acontecer alguma coisa, fico feliz em trabalhar com você para descobrir qual deve ser o comportamento "correto" (ou se esse é um caso extremo que deve ser deixado Indefinido).

Opção 3: usar Oj automaticamente para tudo (apenas Rails 4.1+) ...

Se suportar apenas Rails 4.1 e superior for uma opção, há uma opção um pouco menos invasiva . (Neste caso, provavelmente não deve tentar imitar json gem.) Ele ainda tem o problema de "desviar um pouco no resultado da codificação", mas pelo menos protegeria você do objeto json gem compat State Absurdo.

Todos 55 comentários

Você pode fornecer um teste simples que eu possa executar?

@ ohler55 Vou preparar um teste até o final desta semana.

Posso reproduzir isso sem AMS fazendo um simples select no ActiveRecord. Algo como User.select("users.first_name || ' ' || users.last_name AS full_name, users.*") se comporta da mesma forma.

Estou prestes a fazer um lançamento. Houve algumas mudanças em torno do ActiveSupport. Talvez isso ajude.

De qualquer forma, se não, não tenho o ActiveRecord configurado com banco de dados. Um teste simples seria aquele que usa apenas um certo número de gemas e algum código ruby ​​fornecido por você. Ainda não tenho certeza do que é o JSON esperado ou como reproduzir o que você está fazendo.

Aqui está um aplicativo Rails com o comportamento: https://github.com/Soliah/oj-test. Existem 2 branches, master e no-oj. A única diferença entre os 2 ramos é a inclusão de oj no mestre. A saída esperada são apenas as propriedades do modelo de usuário.

Editar:

Rails 4.1.7, Ruby 2.1.4. http://localhost:3000/users é a rota de teste.

ramo no-oj:

[
   {
      id:5,
      first_name:"John",
      last_name:"Smith",
      email:"[email protected]",
      created_at:"2014-11-03T00:39:55.342Z",
      updated_at:"2014-11-03T00:39:55.342Z"
   }
]

ramo mestre com oj:

{
   klass:"User",
   table:{
      name:"users",
      engine:"User",
      columns:null,
      aliases:[

      ],
      table_alias:null,
      primary_key:null
   },
   values:{

   },
   offsets:{

   },
   loaded:false,
   arel:{
      engine:"User",
      ctx:{
         source:{
            left:{
               name:"users",
               engine:"User",
               columns:null,
               aliases:[

               ],
               table_alias:null,
               primary_key:null
            },
            right:[

            ]
         },
         top:null,
         set_quantifier:null,
         projections:[
            "#<struct Arel::Attributes::Attribute relation=#<Arel::Table:0x007f847f84b308 @name="            users",
            @engine=User(id:integer,
            first_name:string,
            last_name:string,
            email:string,
            created_at:datetime,
            updated_at:datetime),
            @columns=nil,
            @aliases=            [

            ],
            @table_alias=nil,
            @primary_key=nil>,
            name="*">"
         ],
         wheres:[

         ],
         groups:[

         ],
         having:null,
         windows:[

         ]
      },
      bind_values:[

      ],
      ast:{
         cores:[
            {
               source:{
                  left:{
                     name:"users",
                     engine:"User",
                     columns:null,
                     aliases:[

                     ],
                     table_alias:null,
                     primary_key:null
                  },
                  right:[

                  ]
               },
               top:null,
               set_quantifier:null,
               projections:[
                  "#<struct Arel::Attributes::Attribute relation=#<Arel::Table:0x007f847f84b308 @name="                  users",
                  @engine=User(id:integer,
                  first_name:string,
                  last_name:string,
                  email:string,
                  created_at:datetime,
                  updated_at:datetime),
                  @columns=nil,
                  @aliases=                  [

                  ],
                  @table_alias=nil,
                  @primary_key=nil>,
                  name="*">"
               ],
               wheres:[

               ],
               groups:[

               ],
               having:null,
               windows:[

               ]
            }
         ],
         orders:[

         ],
         limit:null,
         lock:null,
         offset:null,
         with:null
      }
   }
}

Não fiz nada de especial na configuração acima, como usar AMS. Apenas uma simples chamada de render json: @users .

Como eu não usei muito o rails, vou levar algum tempo para descobrir como fazer seu teste funcionar e ver qual classe de objeto está sendo serializada.

Fico feliz em ajudar, por favor, me avise se você quiser que eu tente alguma coisa.

Por favor, tente o mais recente. Acabei de lançar 2.11.0.

Se isso não funcionar, se você puder fazer um script ruby ​​simples que crie uma instância do objeto serializado, seria ótimo. O objeto User, pelo que parece.

Acabei de tentar isso no aplicativo Rails e o comportamento é o mesmo.

O que não estou certo é se devo chamar Oj.dump ou apenas some_object.to_json no contexto do Rails. A gema oj_mimic_json lida com isso agora que multi_json não está na foto?

Eu tentei reproduzir isso com ActiveRecord fora do Rails e não consigo obter o mesmo comportamento. Isso me leva a acreditar que tem algo a ver com Rails mais adiante na seção de renderização. Talvez algo aqui: https://github.com/rails/rails/blob/master/actionpack/lib/action_controller/metal/renderers.rb embora eu não tenha certeza.

Posso contornar isso e fazer o que foi feito aqui http://brainspec.com/blog/2012/09/28/lightning-json-in-rails/

Por interesse, aqui está ActiveRecord sem Rails:

source 'https://rubygems.org'


gem "activerecord"
gem "sqlite3"
gem "oj", github: "ohler55/oj"
require 'active_record'
require 'oj'

Oj.mimic_JSON()
# Requiring json here seems to stop conflicts when requiring json in other files.
begin
  require 'json'
rescue Exception
  # ignore
end

ActiveRecord::Base.logger = Logger.new(STDERR)

ActiveRecord::Base.establish_connection(
  :adapter => "sqlite3",
  :database => ":memory:"
)

ActiveRecord::Schema.define do
  create_table :users do |table|
    table.column :first_name, :string
    table.column :last_name, :string
    table.column :email, :string
  end
end

class User < ActiveRecord::Base
end

User.find_or_create_by(first_name: "John", last_name: "Smith", email: "[email protected]")

puts User.first.as_json

O Oj.mimic_JSON deve cuidar de falsificar a gema json. Se não estiver, me avise e eu o recuperarei.

Existem algumas coisas que o ajudarão a entender o que está acontecendo. Oj tem modos diferentes. Se você está tentando serializar um objeto, pode usar o padrão: modo de objeto em Oj. Como você viu, isso cria uma representação JSON com todo o material extra do registro ativo nele. Provavelmente não é o que você quer. Se você puder as_json () e serializar isso usando a abordagem de iluminação que você apontou, isso deve funcionar bem. Observe que uso "deveria".

Aqui está onde pode ficar complicado. o método as_json () está chamando apenas no modo compat. mimic_JSON coloca Oj no modo compat. Tudo bem até agora. Se você tiver a opção: use_to_json definida como true, o dump deve funcionar conforme o esperado. Se for falso, provavelmente não será. Você pode verificar se: use_to_json é verdadeiro?

Outra reviravolta. Se to_json for definido e você chamar isso: use_to_json é automaticamente definido como falso. Isso evita as chamadas recursivas ativas ao chamar to_json. Basicamente, ele chama o serializador (Oj), que deve chamar to_json. Isso continua até que o aplicativo morra.

A chave pode ser adicionar outro: use_as_json ou algo assim. Vamos continuar explorando.

Ok, parece-me que no Rails, mode: :object está sendo usado não importa o que eu faça ao usar render json: @users . Isso pode ser facilmente reproduzido em rails c com User.first.to_json .

Passar opções para to_json não muda este comportamento. Chamar Oj.default_options = { mode: :compat } explicilty também não altera esse comportamento.

No entanto, ir Oj.dump(User.first) se comporta corretamente, então isso se parece com algo com Rails e como JSON é configurado?

Essa é uma informação útil. Passar argumentos para to_json () não funcionará. Você pode definir o modo padrão assim.

  Oj.default_options = { mode: :compat }

Pelo que você descreveu, parece que o problema pode estar no sinalizador: use_to_json sendo definido como falso quando to_json () é chamado. Eu posso ter que mudar isso. Você pode verificar se em Oj.dump (User.first) o método as_json () é chamado?

Colocando um depurador aqui https://github.com/rails/rails/blob/master/actionpack/lib/action_controller/metal/renderers.rb#L117

o seguinte comportamento é observado:

json.to_json retorna https://gist.github.com/Soliah/a5d21eaa77a6762235c1
Oj.dump(json) retorna https://gist.github.com/Soliah/5337e30662a9e2828b1e
Oj.dump(json, mode: :object) retorna https://gist.github.com/Soliah/8cf38eaec18640582af3

O que é interessante é json.to_json e Oj.dump(json, mode: :object) retorna valores diferentes ...

Editar

O objeto json é apenas:

#<ActiveRecord::Relation [#<User id: 5, first_name: "John", last_name: "Smith", email: "[email protected]", created_at: "2014-11-03 00:39:55", updated_at: "2014-11-03 00:39:55">]>

Pelo que você descreveu, parece que o problema pode estar no sinalizador: use_to_json sendo definido como falso quando to_json () é chamado. Eu posso ter que mudar isso. Você pode verificar se em Oj.dump (User.first) o método as_json () é chamado?

Acabei de confirmar que fazer Oj.dump(User.first) chama as_json . as_json não é chamado durante um render json: @users embora.

ok, acho que tenho que permitir as_json. Deixe-me dar uma olhada em alguns dos problemas e ter certeza de que as_json não entre em um loop recursivo sem fim com algumas das outras bibliotecas do Rails. Enquanto isso, posso criar um branch para você com a mudança, se isso ajudar.

Uma última coisa, é que esse problema divergiu potencialmente do problema original com ActiveModelSerializers. Tenho a sensação de que está relacionado, mas talvez não completamente. Eu preferiria que isso funcionasse com AMS, mas isso pode ser outro problema.

Você sabe o que o AMS chama? Tenho certeza de que está relacionado, mas não tenho certeza do caminho que o AMS está tomando.

A versão estável atual 0.9.x usa as_json até onde eu sei: https://github.com/rails-api/active_model_serializers/blob/0-9-stable/lib/active_model/default_serializer. rb # L17

master que será 0.10 é uma grande refatoração pelo que entendi, então a API mudará. @kurko ou @guilleiguaran podem oferecer algumas sugestões.

Experimente o branch as_json-ok. Eu acho que pode estar tudo bem. Tenho uma verificação de segundo nível para recursão. O problema é que o suporte ativo faz com que as_json retorne o próprio objeto em alguns casos, como para tipos de núcleo.

Ok, experimentando 00b368cc33bb893bf9ae23f43f70489f7b79f791 (as_json-ok branch)

class User < ActiveRecord::Base
  def as_json(options = {})
    logger.debug("User as_json called!")
    super
  end

  def to_json(options = {})
    logger.debug("User to_json called!")
    super
  end
end

Com render json: User.all , se eu tiver o seguinte no modelo de usuário, nenhuma instrução de registro será atingida.

No entanto, se eu mudar para render json: User.first , a declaração de registro ocorrerá para to_json . Rails obviamente tem que fazer algo diferente com User.all pois isso retorna uma coleção ao invés de apenas um objeto User .

Em ambos os casos, as_json não é chamado de forma alguma. Além disso, nenhum desses casos retorna a saída JSON correta.

Isso não é bom. Há uma saída, mas não é muito legal. Uma rotina de despejo pode ser registrada. Porém, não é eficiente. Eu suspeito que o problema é que a coleção é um Hash ou um Array e o Active adicionou um método to_json () que chama o JSON.dump () que agora é Oj e Oj então chamaria o to_json no objeto se: use_to_json estivesse habilitado . Funciona bem até esse ponto, mas então o use_to_json ainda é definido como falso ao tentar converter o objeto User para json, mas nesse ponto todos os atributos serão serializados.

Deixe-me dormir sobre isso e ver se consigo encontrar uma solução. Acho que a chave será fazer com que os métodos as_json sejam chamados.

Acho que @chancancode pode ajudar a entender como o to_json / as_json funciona nas versões mais recentes do suporte ativo: smiley:

@chancancode , podemos descobrir uma maneira de fazer isso funcionar?

Vou dar uma olhada em algumas horas: +1:

Obrigado, estarei off-line nessa hora, mas atenderei pela manhã.

Olá!

A situação em que nos metemos é bastante lamentável, pois temos várias bibliotecas JSON brigando entre si sobre nomes de métodos muito genéricos como to_json : decepcionado:

Resumo Rápido

  1. Os desenvolvedores Rails devem sempre substituir #as_json vez de #to_json
  2. Rails espera que a chamada de #to_json sempre passe pelo codificador Rails (codificador Ruby puro personalizado para <= 4.0, json gem para 4.1+), que considera os ganchos #as_json entre outras coisas
  3. Para usar Oj explicitamente, os desenvolvedores devem chamar Oj.dump(obj)
  4. Para usar json gem explicitamente, os desenvolvedores devem chamar JSON.generate(obj) (isso só funciona de forma confiável no Rails 4.1+)
  5. A definição #to_json simplesmente chame rails_json_encoder_encode(self.as_json) em todas as versões (ligeiramente simplificado)
  6. Oj pode / deve invocar #as_json em objetos que não sabe codificar nativamente

Antes de entrarmos em detalhes, aqui estão algumas histórias de como chegamos à situação atual.

Codificador Rails JSON

História Antiga (era Rails 2.3)

Era uma vez, Ruby não vinha com nenhuma biblioteca JSON. Como o Rails precisava gerar objetos JSON, escrevemos nosso próprio codificador e, eventualmente, decidimos pela API baseada em to_json em suas classes, constrói um hash para representar seus dados e chama recursivamente to_json nesse Hash, e você tem a string JSON desejada. (Curiosidade: esse commit também incluiu o decodificador JSON inicial que funcionou essencialmente convertendo JSON em YAML usando gsub para que pudesse ser analisado com Syck. Tempos divertidos!)

Rails 3 era

Em algum ponto, os Rubistas começaram a escrever outras bibliotecas / wrappers JSON que são mais eficientes do que o codificador Ruby puro que vem com Rails, como Oj , yajl , etc. Porém, há um problema - a API to_json não oferece nenhuma maneira de cooperarmos com essas bibliotecas. Como to_json é responsável por retornar a string JSON resultante, não há como esses codificadores alternativos "se conectarem" a esse processo.

A observação principal aqui é que a maioria dos programadores de aplicativos não substituem to_json porque desejam controlar como os objetos Ruby são _encodidos_ em uma string JSON; eles simplesmente desejam descrever como desejam seus dados _representados_ (por exemplo, quais campos incluir, se o objeto deve ser mapeado para um Array ou um Objeto ...).

Assim, as_json nasceu . A ideia é que os desenvolvedores podem simplesmente substituir as_json para retornar a representação Ruby de seus dados (ou seja, retornar um Hash / Array / etc) e deixar que o codificador se encarregue de transformá-lo em uma string JSON.

Por motivos de compatibilidade com versões anteriores, o método #to_json é mantido como um "ponto de entrada" para nosso codificador baseado em Ruby (1.8.7 não tem um codificador JSON na biblioteca padrão). No entanto, agora deve ser possível usar um codificador alternativo, como Oj, por exemplo, chamando Oj.dump(some_rails_object_that_oj_doesnt_know_about) . Nesse caso, o codificador oj pode simplesmente invocar #as_json no objeto para obter uma representação simplificada (esperançosamente *) do objeto que será capaz de codificar.

(* o gancho as_json é fornecido como uma "dica" para o codificador - o desenvolvedor é livre para retornar quaisquer objetos estranhos no gancho e cabe ao codificador decidir o que fazer com eles. Na prática no entanto, o significado de coisas como símbolos, hashes, matrizes, booleanos e strings são bem compreendidos. Isso não precisa ser recorrente, portanto, se o desenvolvedor retornou, digamos, uma matriz de não literais, o codificador ainda deve tentar chamar as_json em seus elementos. Fiz algumas anotações detalhadas sobre como um codificador razoável _deve_ se comportar aqui.)

Rails 4 era

Quando o Ruby 1.9.1 foi lançado, a gema json tornou-se oficialmente parte da biblioteca padrão do Ruby. Desde que o 4.0 abandonou o suporte para Ruby 1.8.7, em teoria tudo deve estar bem a partir de agora. Infelizmente, ele usa a arquitetura #to_json , o que o impedia de ser muito útil dentro do Rails por causa das falhas descritas acima. Assim, as coisas permaneceram praticamente inalteradas.

À medida que a popularidade da gema JSON crescia, outro problema irritante surgiu. Como mencionado acima, a gema json também define um método to_json que não é exatamente o mesmo que vem com o Rails. (por exemplo, um pega um hash de opção, um espera um objeto State ; um considera as_json , o outro não). Isso criou todos os tipos de problemas e confusão .

Rails 4.1+

Peguei a tarefa de limpar algumas dessas bagunças. Algumas coisas mudaram:

  • Anteriormente, Rails e json gem pareciam se dar bem um com o outro até que as coisas explodissem repentinamente em alguns casos extremos. O principal problema é que o JSON gem espera e passa um objeto hash-like-mas-não-realmente State como o segundo argumento para to_json , onde Rails espera um hash real. Para evitar esses problemas, detectamos e contornamos completamente o codificador Rails quando a gema JSON está envolvida. Então, quando você chama ActiveSupport::JSON.encode(obj) ou obj.to_json , você obtém as coisas do Rails, enquanto quando você chama explicitamente JSON.{generate|dump}(obj) você obtém o resultado "básico" do JSON gem sem nenhum dos Rails coisas ( as_json ).
  • Como o Ruby 1.9+ vem com uma gema json , não há mais sentido em manter o codificador Ruby puro dentro do Rails. Eu arranquei todas as coisas relacionadas à codificação dentro do Rails (ou seja, o código que converte o objeto Ruby em strings JSON reais ... por exemplo, nil => "null" ), e simplesmente deslizei a joia JSON . Então, quando você chama to_json , o Rails primeiro chama recursivamente as_json no objeto e passa isso para JSON.generate .
  • Uma vez que os mantenedores do multijson pareciam desinteressados ​​em continuar a manter aquela gema, ela foi retirada do Rails em favor de usar apenas a gema embutida json . Isso afeta apenas a análise! Nas versões anteriores, o Rails usava apenas multi-json no lado da análise. O lado da codificação sempre passou pelo próprio codificador do Rails (por meio de #to_json ) com ou sem multi-json / oj / json gem ativado. Exceto pelos pontos que você usou explicitamente MultiJson.dump(...) , você sempre usou o codificador interno do Rails. Instalar o Oj gem não diz ao Rails para usar o codificador Oj independente das configurações do MultiJson. Qualquer efeito na velocidade de _encoding_ que você notou é provavelmente psicológico :)

Controlador Rails

Quando você chama render json: some_object , o Rails chama #to_json no objeto, que deve passar pelo codificador Rails pronto para usar.

Serializador de modelo ativo

v0.8 e 0.9

Essas duas versões do AM :: S têm uma base de código completamente diferente, mas para o nosso propósito são virtualmente iguais. Quando você faz um render json: object , ele tenta envolver object no objeto serializador apropriado (que implementa as_json ) e passar esse objeto (ainda um objeto Ruby neste ponto) para o renderizador json do controlador, que chama to_json conforme descrito acima.

Se você estiver usando o Rails 4.2, você precisará do 0.8.2 / 0.9.0 para que as coisas funcionem corretamente porque o Rails renomeou alguns dos métodos de gancho do controlador.

v0.10

Esta é outra reescrita completa, mas atualmente define to_json vez de as_json .

( @guilleiguaran precisamos: scissors: estes to_json em AM :: S em favor de as_json como indiquei aqui )

Onde as coisas deram errado com Oj + Rails

A base de código do Rails depende de to_json todas as áreas. Por padrão, isso deve resolver para o codificador JSON do Rails, que usa o gancho as_json . Parece que em algum lugar da gema Oj ele substitui to_json por uma definição que ignora os ganchos as_json (provavelmente aqui ?).

Onde isso nos deixa ...

Opção 1: ativar explicitamente

  1. Configure Oj para ...

    1. Não substituir to_json

    2. Honra as_json

    3. (Acredito que o acima equivale a "não use o modo json gem imitar e habilite o modo compat", mas posso estar errado)

  2. Use explicitamente render json: Oj.dump(obj) e render json: Oj.dump(WhateverSerializer.new(obj)) em seus controladores

É basicamente assim que as coisas sempre funcionaram com as versões anteriores do Oj. (Novamente, o MultiJson nunca o ajudou na codificação.)

Presumivelmente, você pode fazer o monkey patch no renderizador json para fazer isso automaticamente para você (nesse caso, as adições do renderizador AM :: S também devem funcionar ™).

Opção 2: usar Oj automaticamente para tudo ...

Isso requer patch Oj para oferecer um modo que sobrescreveria to_json para codificar objetos de uma maneira compatível com Rails . Isso é super invasivo e você provavelmente acabará lidando com muitos dos mesmos bugs com os quais lidamos antes, então não sei se eu poderia recomendar isso.

Além disso, é bem provável que desviemos um pouco no resultado da codificação, mas se acontecer alguma coisa, fico feliz em trabalhar com você para descobrir qual deve ser o comportamento "correto" (ou se esse é um caso extremo que deve ser deixado Indefinido).

Opção 3: usar Oj automaticamente para tudo (apenas Rails 4.1+) ...

Se suportar apenas Rails 4.1 e superior for uma opção, há uma opção um pouco menos invasiva . (Neste caso, provavelmente não deve tentar imitar json gem.) Ele ainda tem o problema de "desviar um pouco no resultado da codificação", mas pelo menos protegeria você do objeto json gem compat State Absurdo.

wdyt @jeremy? :cara de gozo:

Explicação fantástica. Obrigado.

Eu estava visando a opção 1, com exceção da substituição to_json. A substituição está lá agora para mitigar os rails monkey corrigindo os tipos de núcleo / base. Pode fazer sentido para Oj mudar algumas das opções e seu comportamento para que possa funcionar tanto com o Rails quanto com as bibliotecas ativas.

Vou fazer um branch de Oj que segue a descrição semi-formal para as_json e ver como funciona. Acredito que a principal diferença estará em lidar com o tipo primitivo e o tipo básico.

Me diga como foi! Trate a essência como uma "nota", mais do que qualquer coisa, certamente podemos revisar e corrigir qualquer coisa que não faça sentido.

Quanto ao problema imediato descrito neste tíquete, tenho certeza de que to_json está sendo substituído por uma definição incompatível (como um repórter observou acima, Oj.dump e to_json dá resultados diferentes). Portanto, desligar isso deve resolver o problema.

Estou fazendo um planejamento e gostaria de passar algumas coisas pelo grupo. Não posso remover a substituição de to_json, pois muitos (talvez a maioria) chamam JSON.dump, que cria um loop recursivo, a menos que a chamada to_json desative a chamada Oj de to_json quando encontrar um Object que responde a esse método. A alternativa seria nunca chamar to_json de dentro do Oj. Alguma opinião?

O as_json sempre seria chamado se o Object respondesse a esse método, a menos que seja um primitivo. Vou deixar em cheque um objeto retornando a si mesmo.

Não tenho 100% de certeza se entendi seu problema, você pode explicar ...

  1. Como é a substituição de to_json no Oj?
  2. Qual é a aparência da sequência de chamadas problemática?

Aqui está como a substituição de to_json parece nas versões recentes do Rails (4.1+).

  1. O codificador Rails não considera to_json todo - nós apenas usamos as_json para serialização, e to_json é apenas um ponto de entrada para o codificador.
  2. Nossa postura é que quando as pessoas fazem um JSON.dump explícito (em vez de to_json ), as pessoas provavelmente esperam obter a saída de json gem (ou seja, usa to_json vez de as_json ), então contornamos o codificador Rails completamente.

Suspeito que o ponto número 1 seja o motivo pelo qual não temos o mesmo problema com o qual você está lidando.

Para o ponto número 2, fomos capazes de fazer isso porque json gema to_json tem um único (e incompatível) assinatura do método - ele passa a ::JSON::State objeto em vez de um hash para o método. Então, quando vemos que o argumento é um objeto ::JSON::State , sabemos que ele está vindo de JSON.dump ou JSON.generate , caso em que apenas adiamos a definição original de to_json sem a substituição do Rails.

Acho que parte do problema é que estou tentando ser compatível tanto com os trilhos mais antigos quanto com os mais recentes.

Talvez o truque seja o objeto :: JSON :: State. Não tenho certeza se isso funciona ainda.

É claro que to_json e as_json precisam ser considerados separadamente. Deixe-me tentar montar algo e ver como funciona. Não tenho certeza se chegarei hoje à noite até amanhã.

Simplesmente não desligar as_json com to_json parece ser o suficiente. Eu tenho que montar um teste com uma coleção ActiveSupport embora. Se alguém tiver um simples que eu possa usar, seria ótimo. Caso contrário, vou fazer alguma pesquisa na web e escrever algo amanhã à tarde.

Estou me perguntando se a abordagem de yajl json funcionaria aqui ou se é o mesmo problema:
https://github.com/brianmario/yajl-ruby/tree/master/lib/yajl/json_gem

Eu prefiro não adotar a abordagem yawl / json_gem, pois força todos os objetos a usarem to_s como uma representação json se ActiveSupport não estiver definido. Nem todos os usuários Oj estão usando ActiveSupport.

Aqui está um teste simples que funciona, mas não tenho certeza de como garantir que Oj seja usado para a chamada to_json sem usar Oj.mimicJSON. Parece fazer a coisa certa no branch as_json-ok.

require 'sqlite3'
require 'active_record'
require 'oj'

#Oj.mimic_JSON()
Oj.default_options = {mode: :compat, indent: 2}

#ActiveRecord::Base.logger = Logger.new(STDERR)

ActiveRecord::Base.establish_connection(
  :adapter => "sqlite3",
  :database => ":memory:"
)

ActiveRecord::Schema.define do
  create_table :users do |table|
    table.column :first_name, :string
    table.column :last_name, :string
    table.column :email, :string
  end
end

class User < ActiveRecord::Base
end

User.find_or_create_by(first_name: "John", last_name: "Smith", email: "[email protected]")
User.find_or_create_by(first_name: "Joan", last_name: "Smith", email: "[email protected]")

puts "as_json - #{User.first.as_json}"
puts "to_json - #{User.first.to_json}"
puts "Oj.dump - #{Oj.dump(User.first)}"
puts "Oj.dump all - #{Oj.dump(User.all)}"

A versão mais recente corrige isso?

Vou dar isso atrás e relatar de volta.

O oj_mimic_json ainda é necessário?

a chamada mimic_JSON é necessária se você estiver fazendo chamadas JSON ou chamando to_json (). Se você estiver usando Oj diretamente, mimic_JSON não é necessário.

Trabalhando sozinho com active_model_serializers usando o HEAD atual. Carregar cerca de 10.000 registros em desenvolvimento leva cerca de meio segundo para renderizar:

Sem oj: Completed 200 OK in 3140ms (Views: 3061.0ms | ActiveRecord: 78.4ms)
Com oj: Completed 200 OK in 2483ms (Views: 2406.5ms | ActiveRecord: 76.3ms)

Os objetos estão sendo serializados corretamente com ou sem AMS.

Excelente!

Para conjuntos de dados menores, a diferença de tempo de renderização não é particularmente diferente do que a biblioteca JSON embutida. Existe uma maneira de confirmar se oj está sendo usado?

Tente definir o recuo das opções padrão para 2 e observe a saída.

Ok, isso confirma que oj está definitivamente funcionando. : coração:: brilha:

ótimo, podemos encerrar esse problema?

Sim. Seria ótimo ter uma nova versão do Rubygems.

2.11.1 já existe. Qual versão você está usando?

Oh, desculpe, eu estava apenas baixando o HEAD atual no master (374c1133fb412953cc52ea82884e26bbfbf7c571). A versão em Rubygems não me dá o JSON certo de volta.

Isso é estranho. O código é o mesmo, exceto por algumas mudanças travis. E as mudanças para carros alegóricos esta noite.

Desculpe, parece que estou referenciando a versão errada no meu Gemfile.lock. Bom fechar!

ótimo obrigado

@chancancode @Soliah Como isso corrige o problema de AMS?

Esta página foi útil?
0 / 5 - 0 avaliações