Saltar al contenido
AZ Tools

Decodificador BSON y visor de Extended JSON

BSON es la forma binaria que MongoDB almacena y habla: un recuento de bytes int32 en little-endian, una sucesión de elementos — cada uno con un código de tipo, un nombre de campo terminado en NUL y un valor cuya longitud decide el tipo — y un único byte cero para cerrar. Este decodificador lee esos bytes directamente en tu navegador y muestra el documento de tres maneras a la vez: un árbol de campos que nombra el tipo de cada elemento junto con su byte de tipo y dice lo que cuesta en bytes, Extended JSON canónico y Extended JSON relajado. Se manejan los veintiún tipos de elemento que define la especificación, incluidos los obsoletos que solo aparecen en datos antiguos: undefined (0x06), DBPointer (0x0C), symbol (0x0E) y el subtipo binario 0x02, que lleva una segunda longitud dentro de su propia carga y entrega cuatro bytes de basura a cualquier lector que la olvide. Los números son donde un decodificador dentro del navegador suele fallar. int64, la fecha UTC y el timestamp interno de MongoDB son de 64 bits, y un número de JavaScript solo guarda enteros exactos hasta 9.007.199.254.740.991, de modo que 9223372036854775807 se convierte en silencio en 9223372036854775808 en cuanto pasa por uno; aquí los tres se leen y se imprimen desde un BigInt. decimal128 se decodifica desde su propio coeficiente de 128 bits y su exponente sesgado hasta una cadena decimal exacta, sin aproximarlo nunca con un double, que es justamente la razón por la que el tipo existe para dinero. El árbol también abre un ObjectId en las tres partes que realmente tiene — cuatro bytes de fecha de creación, cinco aleatorios y un contador de tres — y muestra una fecha UTC tanto como su recuento de milisegundos con signo como en forma de fecha, para que los números negativos de las fechas anteriores a 1970 dejen de sorprender. Lo que no hace es adivinar: un documento cuya longitud declarada no cuadra con el archivo, uno que nunca llega a su terminador, uno con un byte de tipo que la especificación no define, o un archivo JSON entregado por error, se rechaza con nombre y desplazamiento en bytes en lugar de convertirse a medias en un árbol verosímil.

Cómo usar

  1. Suelta un archivo .bson en la caja: la salida de mongodump, un documento suelto guardado por un controlador o cualquier cosa que haya escrito una herramienta de MongoDB.
  2. Lee el árbol. Cada fila es un elemento con su nombre de tipo BSON, el byte de tipo tal como viaja, su valor y los bytes que cuesta, así que un documento demasiado grande se explica solo.
  3. Usa el Extended JSON canónico cuando los tipos tengan que sobrevivir — $numberInt y $numberLong no son lo mismo — y el relajado cuando solo quieras leer los datos.
  4. Mira la línea extra bajo un ObjectId para su fecha de creación, sus bytes aleatorios y su contador, y la de una fecha UTC para ver la fecha detrás del recuento de milisegundos.
  5. Si el archivo se rechaza, fíjate en qué error es: una longitud que no cuadra, un terminador ausente y un byte de tipo indefinido son tres fallos distintos, y el desplazamiento dice dónde mirar.

Preguntas frecuentes

¿Por qué un array BSON ocupa mucho más que el mismo array en JSON?
Porque BSON no tiene un tipo array propio. Un array se guarda como un documento corriente cuyos nombres de campo son los índices decimales "0", "1", "2" y así, cada uno una cadena real terminada en NUL. Mil enteros de 32 bits cuestan entonces un byte de tipo, un nombre de índice de uno a tres caracteres, un terminador y cuatro bytes de dato cada uno: unos 8,9 KB donde los valores solos son 4 KB. También es la razón de que las claves de índice puedan estar mal sin que nada lo note: los controladores leen los arrays por posición, así que uno con claves "0", "2", "x" se decodifica en tres elementos y solo falla cuando algo lo vuelve a serializar. Esta herramienta señala ese caso en lugar de disimularlo.
¿Qué diferencia hay entre el Extended JSON canónico y el relajado?
Extended JSON es la forma estándar de escribir BSON como texto, y viene en dos variantes porque sirven a dos trabajos distintos. El canónico envuelve cada valor en un marcador de tipo — {"$numberInt": "1"} no es {"$numberLong": "1"} ni {"$numberDouble": "1.0"} — de modo que un documento puede salir a texto y volver como exactamente los mismos bytes. El relajado quita la envoltura allí donde el JSON simple puede sostener el valor: los números son números y una fecha dentro del rango de ISO 8601 se convierte en un instante legible. El relajado se lee mucho mejor y es lo que escribe mongoexport por defecto, pero pierde información: una vez que un 1 es solo un 1, nada dice de cuál de los tres tipos numéricos venía.
¿Qué valores de BSON no puede representar JavaScript y qué se hace aquí con ellos?
Tres. int64 y la fecha UTC son enteros con signo de 64 bits, y el timestamp interno son dos mitades de 32 bits sin signo en una palabra de 64, mientras que un número de JavaScript es un double y solo guarda enteros exactos hasta 9.007.199.254.740.991. Cualquier valor mayor se redondea al entrar, y así es como 9223372036854775807 se vuelve 9223372036854775808 en tantos visores. Este decodificador lee los tres con BigInt e imprime los dígitos desde el BigInt, de modo que nada pasa por un double. Con decimal128 es aún peor: es un tipo decimal precisamente para que 0,1 y 2,675 sigan siendo exactos, y leerlo en un double anula el motivo de haberlo elegido. Aquí se decodifica desde su coeficiente de 128 bits y su exponente sesgado directamente a una cadena decimal.
¿Por qué una fecha anterior a 1970 aparece como un número negativo?
Porque es exactamente lo que guarda BSON. El tipo fecha UTC son milisegundos int64 desde la época Unix, y el entero tiene signo, así que cualquier instante anterior al 1970-01-01 es un recuento negativo: una fecha de nacimiento de 1953 ronda los −526.608.000.000. Es legal y todos los controladores lo devuelven intacto, pero un número sorprendente de herramientas tratan el campo como sin signo o lo recortan en cero, y así una fecha de 1953 acaba en 1970 o salta al año 292 millones. El rango que el tipo admite es mucho más ancho que el calendario que soporta la mayoría del software, por eso aquí se muestra el recuento de milisegundos junto a la fecha y se dice con claridad cuándo un valor queda fuera de lo que una fecha de calendario puede expresar.
¿Qué es el subtipo binario 0x02 y por qué se le llama trampa para analizadores?
Es la disposición binaria original, hoy obsoleta. Todo campo binario se escribe como una longitud int32, un byte de subtipo y luego la carga — salvo el subtipo 0x02, cuya carga empieza con un segundo int32 que repite la longitud, cuatro menos que el primero. Un lector que lo trate como a cualquier otro subtipo devuelve un blob con cuatro bytes de más pegados delante, y como esos bytes parecen datos binarios plausibles nadie protesta; la corrupción solo asoma cuando alguien intenta usar el valor. Las colecciones antiguas de la era anterior a la 2.0 todavía los contienen, así que un decodificador que dice leer BSON tiene que tratarlo aparte. Este lo hace, y rechaza el campo por completo cuando la longitud interior no concuerda con la exterior.
¿Qué hay dentro de un ObjectId? ¿Es un UUID?
No: son 12 bytes con estructura, y conocer esa estructura suele ser el motivo de mirarlo. Los cuatro primeros bytes son una marca de tiempo Unix en segundos, big-endian, y por eso ordenar por _id ordena aproximadamente por fecha de creación y se puede filtrar por fecha sin un campo aparte. Los cinco siguientes son aleatorios por proceso, y los tres últimos un contador que arranca en un valor aleatorio y aumenta con cada documento. Las versiones antiguas de MongoDB ponían en medio un identificador de máquina y un id de proceso, lo que filtraba algo sobre el servidor; eso cambió en la 3.4. No hay suma de verificación ni campo de versión, así que cualquier cadena de 24 caracteres hexadecimales es un ObjectId sintácticamente válido, y solo la marca de tiempo incrustada dice si es verosímil.

Herramientas relacionadas