Oj: JSON.parse vs Oj.load tratamiento BigDecimal

Creado en 8 dic. 2020  ·  40Comentarios  ·  Fuente: ohler55/oj

¡Hola, y gracias por mantener Oj !

Estamos luchando un poco para analizar los valores JSON con alta precisión correctamente como BigDecimal (en lugar de Float).

  • Rieles 6.0.3.4
  • DO 3.10.15

_nuestro inicializador_

require "oj"

# none of these 4 settings seems to change the behavior
Oj::Rails.set_encoder
Oj::Rails.set_decoder
Oj::Rails.optimize
Oj::Rails.mimic_JSON

Oj.default_options = {
  trace: true, # debugging
  mode: :compat,
  allow_blank: true,
  bigdecimal_as_decimal: false, # dump BigDecimal as a String
  bigdecimal_load: :bigdecimal, # convert all decimal numbers to BigDecimal
  empty_string: false,
  second_precision: 6,
  time_format: :ruby,
}

Cuando se usa Oj.load , funciona como se esperaba. Teníamos la impresión de que Oj sobrecarga JSON.parse , que por lo tanto debería comportarse igual, pero no lo hace:

json = %Q({ "amount": 100000000000000.01 })


JSON.parse(json)

#0:     compat.c: 72:Oj:}: start_hash
#0:     compat.c:157:Oj:-:   set_number Float
#0:     compat.c: 98:Oj:{: hash_end Hash
#0:     strict.c: 36:Oj:-: add_value Hash

# => { "amount" => 100000000000000.02 } # losing precision

JSON.parse(json)["amount"].class

# => Float < Numeric


Oj.load(json)

#0:     compat.c: 72:Oj:}: start_hash
#0:     compat.c:157:Oj:-:   set_number BigDecimal
#0:     compat.c: 98:Oj:{: hash_end Hash
#0:     strict.c: 36:Oj:-: add_value Hash

# => { "amount" => 100000000000000.01 }

Oj.load(json)["amount"].class

# => BigDecimal < Numeric # correctly treating the number as a BigDecimal

Entonces nuestras preguntas:

  1. ¿Es esto un problema de configuración, un error o no entendemos _ "reemplazo directo" _?
  2. ¿Se supone que debemos reemplazar cada llamada explícita a JSON.parse por Oj.load ?
  3. ¿Hay alguna forma de lograr que JSON.parse comporte igual que Oj.load y respete nuestra configuración para tratar los números decimales como BigDecimals, sin pasar este parámetro explícitamente a cada llamada?

Comentario más útil

Empezaré con eso entonces.

Todos 40 comentarios

No he comprobado la compatibilidad de JSON gem o Rails recientemente, así que tal vez hayan cambiado. En versiones anteriores no había opciones para convertir un decimal en un BigDecimal, por lo que esas opciones para Oj no se aplican a los modos rails o compat. Si eso ha cambiado, será necesario actualizar Oj, pero verifiqué la versión 2.3.1 de la gema JSON y todavía devuelve un Float. ¿Puede decirme qué opciones se están configurando para lanzar la gema JSON para devolver un BigDecimal?

Seguro :-)

json = %Q({ "amount": 100000000000000.01 })

JSON.parse(json, decimal_class: BigDecimal)

# => { "amount" => 100000000000000.01 } # precise result, treating value as BigDecimal

Muy útil, gracias. Obtendré esa opción vinculada a la opción Oj y la abriré para usar con el modo compat.

Empujé una rama llamada decimal-class que admite decimal_class. Necesita pruebas unitarias, pero si quieres probarlo, sería genial.

Hola Peter, gracias por la rápida reacción. Probé la rama en nuestro proyecto, agregué: decimal_class: BigDecimal, a default_options , pero desafortunadamente el comportamiento no cambió. JSON.parse todavía trata el número como Float .

El decimal_class se puede usar con Oj.load o JSON.parse, pero para las opciones predeterminadas se debe usar el anterior bigdecimal_load. Sin embargo, veo que aún no he configurado las opciones predeterminadas, así que no se moleste en comprobarlo todavía. Nos ocuparemos de eso esta noche.

Envió una actualización. La configuración: bigdecimal_load a: bigdecimal en las opciones predeterminadas ahora tendrá efecto en JSON.parse.

Genial 😃 con la rama decimal-class@b86bdcd ahora funciona como se esperaba.

json = %Q({ "amount": 100000000000000.01 })

JSON.parse(json)
# => { "amount" => 100000000000000.01 }

JSON.parse(json)["amount"].class
# => BigDecimal < Numeric

Este también parece ser el comportamiento predeterminado ahora, solo cuando se configura:

Oj.default_options = {
  decimal_class: Float,
  bigdecimal_load: :float # using one of the options is sufficient
}

Podría provocar el error de redondeo / pérdida de precisión / manejo del número como flotante. En todos los demás casos, devolvió BigDecimals ahora. Si bien creo que esta es la forma correcta de manejarlo, no estoy seguro de si esto podría ser un cambio radical.

No probé con configuraciones conflictivas, pero ese es un caso extraño y explicar esto en el archivo README debería ser suficiente.

¡Gracias por la solución rápida!

Lo reclamo como una corrección de errores porque me perdí la opción decimal_class. No estoy seguro de cómo lo encontró, ya que no aparece en la documentación. Pero gracias. Tengo una corrección de errores más para el modo rieles y luego la lanzaré a menos que lo necesite de inmediato.

Gracias, tenemos nuestra solución en este momento. Sin embargo, me sorprendió que lo necesitáramos. Y como no abarca todos los lugares posibles, estaremos encantados de actualizar a la nueva versión pronto.

Yo mismo no encontré esta opción, mi colega @padde la usó en nuestra solución. Estaba investigando una solución para este problema exacto (antes de agregar Oj al proyecto) y debe haberlo encontrado en las redes.

Solo encontré estos PR más adelante: https://github.com/flori/json/issues/219 que apunta a https://github.com/flori/json/issues/223 donde la opción decimal_class está implementado.

Oye, no estoy seguro de cómo lo encontré, para ser honesto, probablemente un caso de hurgar hasta que funcionó.

También encontré https://github.com/ruby/ruby/pull/2630 donde alguien quería agregar la opción a los documentos de Ruby. Fueron dirigidos a flori / json, ya que las instantáneas de esto se agregan al núcleo de ruby ​​aparentemente y el desarrollo principal tiene lugar allí. Sin embargo, parece que los documentos nunca se agregaron allí, al menos no en las opciones del método JSON.parse . Hay una noción de la opción en los comentarios de la documentación del analizador subyacente , aunque no estoy seguro de si terminan en documentos generados.

Bueno, gracias a ustedes, la función estará en Oj.

La versión 3.10.17 soluciona el problema.

Con la versión 3.10.17, estaba viendo un montón de errores que aparecían en ActionCable y Sidekiq, donde aparecían cadenas en el lugar de los flotadores, en lo que respecta al tiempo, por ejemplo, Time.current.to_f . Aún diagnosticando esto, pero una advertencia para alguien ahí fuera. He vuelto a la versión 3.10.16.

Mantenme informado. Si hay un error que solucionar, lo solucionaré.

No creo que sea un error. Pero como mencioné anteriormente, es un cambio radical. Aconsejaría no configurar la conversión BigDecimal como predeterminada, sino que se adhiera a Float para ello.

No cambié ningún código. Mi configuración de Oj es bastante básica:

require 'active_support/core_ext'
require 'active_support/json'
require 'oj'

Oj.optimize_rails

ActiveSupport::JSON::Encoding.time_precision = 6

Y aquí hay dos errores que se activan dentro de Sidekiq y Rails. También veo errores similares de 'Float esperado, tengo String' en el código de mi aplicación.

Screen Shot 2020-12-17 at 11 16 26 AM
Screen Shot 2020-12-17 at 11 18 00 AM

Sé que esto no ayuda mucho ... todo lo que sé en este momento es que 3.10.17 causó estos dos errores sin cambiar nada.

Interesante. Parece que ambos contamos con que se carguen grandes cantidades como cadenas o flotantes. Avísame si se te ocurre algo necesario para dar tanto bigdecimal como apoyo para el compañero.

El problema ocurre cuando Time.current.to_f devuelve un flotante de 17 dígitos en lugar de un flotante de 16 dígitos. Cuando tiene 17 dígitos, JSON.parse devuelve un BigDecimal; de lo contrario, devuelve un Float. Y me imagino que cuando BigDecimals se serializa en argumentos de trabajo en Sidekiq (y lo que sea que esté haciendo Rails), termina serializando las Big D como cadenas ( to_s ) y rompe esas bibliotecas (intermitentemente). No he analizado demasiado en profundidad lo que está sucediendo en este tema, tal vez este nuevo enfoque sea correcto, sin embargo, parece ser un cambio importante tanto para Sidekiq como para Rails.

Lo que nos encontramos es con los límites de precisión en un flotador. Para dobles, la precisión varía de 15 a 17 lugares. El problema es que si el análisis utiliza demasiados dígitos antes de asumir un BigDecimal, los flotantes no serán precisos y, si son muy pocos, obtendrá un BigDecimal cuando se espera un flotante. Hay dos requisitos en competencia. Uno es para BigDecimal en algunos casos y algunos para flotantes para el mismo número. Si alguien tiene algunas ideas sobre cómo abordar esto, hablemos de ellas.

Dado que los valores predeterminados de Oj no cambiaron, me pregunto si uno de los paquetes está modificando los valores predeterminados.

Dado que los valores predeterminados de Oj no cambiaron, me pregunto si uno de los paquetes está modificando los valores predeterminados.

Creo que cambió los valores predeterminados de Float (un número) a BigDecimal (una cadena):

https://github.com/ohler55/oj/issues/628#issuecomment -742485671

Eso fue involuntario. El lugar de los valores predeterminados no cambió, por lo que debe haber alguna otra interacción que me perdí. Lo encontraré y lo devolveré a la forma en que estaba.

En caso de que esto ayude a esta investigación en absoluto, mientras intentamos actualizar, encontramos que el código del marco se estaba pasando inesperadamente BigDecimal s devuelto después de la actualización. Todo el flujo es interno de Capybara, por lo que no estoy seguro de cómo está sucediendo exactamente, pero el único cambio que hicimos fue el aumento de 3.10.16 -> 3.10.17.

Muchas gracias por todos sus esfuerzos aquí, esta joya es fantástica.

Gracias, eso podría ayudar. Si puede recrear el problema con la suficiente facilidad, ¿estaría dispuesto a ejecutar una versión de depuración para que pueda ver dónde se está configurando?

Desenredar esto de nuestra aplicación es difícil, pero parece estar analizando coordenadas x / y donde hay un número decimal de gran precisión. Estoy tratando de ver si puedo aislar estas partes en algo que podamos compartir públicamente.

De hecho, creo que este problema es un Ruby BigDecimal que se maneja de manera diferente después de ser analizado en JSON, que podría ser lo opuesto al problema citado aquí.

Ayudaría, pero puedo entender que puede que no sea fácil.

Después de repasar el código, parece que una posible razón para el cambio aparentemente predeterminado es que, anteriormente, cada llamada al análisis JSON obligaba a que la carga BigDecimal fuera flotante. Ahora se usa la opción predeterminada, pero esa opción predeterminada está configurada para flotar cuando se configura el modo mímico. Me pregunto si el valor predeterminado se cambiará en alguna parte. ¿Alguna posibilidad de que eso esté sucediendo?

@ ohler55 Creo que eso podría ser correcto.

Todavía estoy trabajando en la extracción, pero los puntos en los que esto parece estar fallando para nosotros están aquí:
https://github.com/twalpole/apparition/blob/ba431173024c56354a86ef8369c3493e8a1a39bb/lib/capybara/apparition/driver/chrome_client.rb

Este es un controlador para ejecutar pruebas de navegador. Este fragmento de código ejecuta algo de JavaScript en el navegador y luego pasa JSON (como una cadena) desde el navegador al controlador para su análisis. (Termina usándose para averiguar qué coordenadas x / y en la pantalla necesitan ser "presionadas" por el mouse para interactuar con un elemento como un botón o enlace, etc.) A partir del 3.10.16 esto se comportó como de costumbre, pero en 3.10.17 y 3.10.18 explota porque el tipo de datos de las coordenadas x / y ha cambiado. De los documentos de Chrome CDP, parece que estas coordenadas simplemente deben ser números (no un tipo específico de número), pero ahora vienen como cadenas:

https://chromedevtools.github.io/devtools-protocol/tot/Input/#method -dispatchMouseEvent

Espero tener unos minutos para crear una pequeña aplicación con una prueba para recrear esto, pero al ejecutar el seguimiento de fallas en nuestro CI y mirar el código de esta gema, estas llamadas JSON.parse se ven así ellos son los que se han visto afectados.

Déjame saber cuál es el resultado de tus pruebas. De una forma u otra conseguiremos que esto funcione.

Actualización aquí: pasamos de Aparición para usar un controlador de Chrome basado en Ferrum (Cuprite, biblioteca diferente pero estilo de comunicación muy similar) y estamos viendo los mismos tipos de errores en las coordenadas a pesar de un código completamente diferente en la biblioteca. Con suerte, ayuda un poco porque no es algo específico de una biblioteca.

Se acerca un día de I + D y voy a intentar crear un PoC muy pequeño para este problema.

Un PoC sería muy útil.

@ ohler55 , algunos comentarios sobre este cambio ... En resumen, esto también nos ha afectado y hemos tenido que volver a 3.10.16 por el momento.

Con bigdecimal_load: :bigdecimal , estamos viendo 2-3 problemas con mongoid:

1) mongoid no maneja bien BigDecimals para los campos de tiempo (por ejemplo: field :updated_at, type: Time ). Podría decirse que es un error de larga data en el mongoide, pero este cambio en oj lo desencadena.

2) También estamos viendo problemas con mongoid muy similares a los problemas de sidekiq descritos, donde Time # to_f a veces devuelve 17 dígitos, lo que hace que mongoid convierta incorrectamente los objetos Time en cadenas.

3) .17+ también da como resultado que mongoide persista un par de BigDecimals como cadenas en otros lugares (no en los campos de tiempo), pero eso podría ser una limitación de esos campos en particular (que pueden contener varios tipos y, por lo tanto, no pueden forzar la recarga del tipo).

Al revisar nuestro código base, creo que hemos estado confiando sin saberlo en el análisis sintáctico de Oj.load como: bigdecimal, mientras que simultáneamente esperábamos que Rails y JSON.parse analizaran como: float. El problema clave parece ser que otras bibliotecas y gemas también dependen de que JSON.parse produzcan constantemente un flotador.

Sostengo que el comportamiento de división anterior podría haber sido deseable, a pesar de que a primera vista parece un error que debería corregirse. Con 3.10.17+, dada la interacción con gemas externas (especialmente con objetos Time, pero no solo esos), parece que Oj.mimic_JSON efectivamente no se puede usar en combinación con bigdecimal_load: :bigdecimal (a través de default_options todos modos). En cambio, el valor predeterminado debe establecerse en :float para compatibilidad con varias gemas y necesitamos anular selectivamente a :bigdecimal en cada llamada para analizar / cargar. Podría decirse que esto es correcto, pero es un fastidio tener que hacer tal cambio en el lanzamiento de un parche.

Mi sugerencia sería restaurar el comportamiento anterior y agregar una versión separada de bigdecimal_load que solo afecta al modo mimic / JSON.parse y por defecto es: float. De esta manera, personas como OP pueden tener selectivamente una carga universal: bigdecimal, mientras que el comportamiento predeterminado preservaría el funcionamiento adecuado con sidekiq, mongoid, capibara, etc.

Si está comprometido a mantener el comportamiento "fijo", le recomiendo que agregue una gran advertencia tanto al registro de cambios como a los documentos regulares sobre el uso de bigdecimal_load: :bigdecimal en combinación con Oj.mimic_JSON .

Decida lo que decida, gracias por sus años de trabajo en oj. ¡Hace una gran diferencia de rendimiento!

Esa es una gran explicación. Me gusta mantener Oj lo más compatible posible, así que trabaja conmigo en esto y podemos resolverlo.

Déjame ver si puedo reformular para asegurarme de que entiendo el problema correctamente.

  1. La gema JSON siempre devuelve un flotante a menos que se utilice la opción secreta :decimal_class .
  2. Al reutilizar Oj :bigdecimal_load esa opción de gema JSON se estaba configurando cuando no se deseaba.
  3. Una solución sería no vincular el Oj :bigdecimal_load al JSON :decimal_class sino tener una opción Oj separada para representar esa opción.

¿Correcto?

Sí, creo que eso es correcto.

Empezaré con eso entonces.

La rama "compact-decimal" está lista para un poco de prueba si desea ejecutarla a través de los ritmos.

@ ohler55 la compat-decimal resolvió las fallas en nuestro conjunto de pruebas. ¡Gracias por sus esfuerzos en esto a pesar de mi incapacidad para proporcionar mucha telemetría!

Entre todas las personas que intervinieron, fue suficiente para vender el problema. ¿Alguien más tiene resultados para compartir? Si no, lo lanzaré en uno o dos días.

@ ohler55 , nuestras pruebas también pasan con compat-decimal . ¡Hurra!

¿Fue útil esta página
0 / 5 - 0 calificaciones

Temas relacionados

ericmwalsh picture ericmwalsh  ·  25Comentarios

NickHurst picture NickHurst  ·  8Comentarios

trevorrowe picture trevorrowe  ·  3Comentarios

swiknaba picture swiknaba  ·  9Comentarios

gottfrois picture gottfrois  ·  13Comentarios