2012-09-25 45 views
21

Tengo un montón de archivos JSON generados automáticamente que quiero almacenar en el control de versiones. El problema es que cada vez que se serializan los archivos, los atributos salen en un orden diferente, lo que dificulta saber si los archivos realmente han cambiado y cuáles son las diferencias reales.Canonicalización de archivos JSON

¿Alguien sabe de una herramienta de código abierto existente que realizará esta tarea?

En su defecto, ¿alguien sabe de una biblioteca JSON con un analizador y un generador que se puede configurar para producir JSON "bonita" con los atributos en (por ejemplo) orden léxico? (Una biblioteca Java o Ruby sería ideal, pero otros clientes potenciales también son bienvenidos.)

Respuesta

13

Python's JSON module es muy utilizable a partir de otros programas:

generate_json | python -mjson.tool > canonical.json 
+1

Gracias. Eso hará el trabajo bien. –

+8

Nota: esto funciona porque 'sort_keys = True' en la implementación de' json.tool', pero esta garantía no parece estar documentada en ninguna parte, por lo que puede valer la pena escribir su propia secuencia de comandos que pueda garantizar: 'import json, sys; imprimir json.dumps (json.load (sys.stdin), sort_keys = True) ' –

0

No he probado muchas combinaciones, pero parece que google-gson mantiene el orden de los atributos en JSON.

retira un ejemplo aquí, ya que no era más pertinente

Sé por experiencia en proyectos anteriores que es muy personalizable, por ejemplo si el base object no es suficiente, se puede utilizar GsonBuilder para crear más complicado adaptadores.

que sin embargo no han probado esto extensivamente con el uso de los casos, pero debe ser simple para comprobar si tiene el resultado esperado

ACTUALIZACIÓN

en lugar de utilizar SVN/CVS para comprobar si sus archivos fueron modificados, encontré que GSON ha incorporado en versioning support que puede o no puede hacer frente a su problema, de sus documentos:

varias versiones de un mismo objeto puede mantenerse mediante el uso de @Since anotación. Esta anotación se puede usar en Clases, Campos y, en una versión futura, Métodos. Para aprovechar esta característica, debe configurar su instancia de Gson para ignorar cualquier campo/objeto que sea mayor que algún número de versión. Si no se establece ninguna versión en la instancia de Gson, serializará y deserializará todos los campos y clases independientemente de la versión.

ACTUALIZACIÓN

La única otra cosa que puedo pensar es analizar sus archivos externos con rhino y el uso de JSON.stringify para convertir el JSON analizado de nuevo a una cadena, entonces usted puede estar seguro de que tiene ejecutar a través de un único 'analizador' y la salida no será diferente.

A continuación, puede detectar posibles cambios.

+0

La anotación @since es poco probable ayudar. Estos archivos JSON provienen de una fuente externa (no Java). –

+0

@StephenC, entiendo su problema más claramente ahora, vea mi actualización – epoch

0

La biblioteca Java de código abierto Jackson puede requerir un cierto esfuerzo de configuración, pero es capaz de imprimir bastante y tiene una anotación bastante clara @JsonPropertyOrder que admite orden alfabético o de salida especificada manualmente.

+1

¿Se puede usar esa anotación en JSON que carga como un árbol de instancias 'TreeNode'? No tengo clases Java Pojo ... o un esquema JSON para jugar. Estos son archivos genéricos JSON con una estructura y contenido impredecible. –

+0

Lamentablemente, no he seguido ese camino. La parte "podría tomar un poco de esfuerzo" en mi respuesta estaba allí con la definición de POJO y el mecanismo de SerializaciónConfig de Jackson en mente. –

0

Ruby 1.9+ mantiene el orden de inserción de hashes, y JSON para 1.9+ honores.

asdf = {'a' => 1, 'b' => 2} 
asdf.to_json # => "{\"a\":1,\"b\":2}" 

asdf = {'b' => 1, 'a' => 2} 
asdf.to_json # => "{\"b\":1,\"a\":2}" 

Aquí es cómo generar un formato "bonita":

asdf = {'a' => 1, 'b' => 2} 
puts JSON.pretty_generate(asdf) 
{ 
    "a": 1, 
    "b": 2 
} 

asdf = {'b' => 1, 'a' => 2} 
irb(main):022:0> puts JSON.pretty_generate(asdf) 
{ 
    "b": 1, 
    "a": 2 
} 

... los mismos atributos se insertan en un orden diferente ...

Esto no tiene mucho sentido para mí, pero voy a tomar una foto.

Dado que Ruby mantiene el orden de inserción, no es demasiado importante el orden de los datos si se crea el hash en un orden determinado; Forzar el orden de clasificación de las teclas y regenerar el hash, y pasar a JSON que:

require 'json' 

puts Hash[{'a' => 1, 'b' => 2}.sort_by{ |a| a }].to_json 
=> {"a":1,"b":2} 

puts Hash[{'b' => 2, 'a' => 1}.sort_by{ |a| a }].to_json 
=> {"a":1,"b":2} 

puts Hash[{'b' => 2, 'c' => 3, 'a' => 1}.sort_by{ |a| a }].to_json 
=> {"a":1,"b":2,"c":3} 

puts Hash[{'b' => 2, 'c' => 3, 'a' => 1}.sort_by{ |a| a }].to_json 
=> {"a":1,"b":2,"c":3} 

puts Hash[{'a' => 1, 'c' => 3, 'b' => 2}.sort_by{ |a| a }].to_json 
=> {"a":1,"b":2,"c":3} 
+0

Mantener el orden de inserción no es suficiente si los mismos atributos se insertan en un orden diferente. (De hecho, estos hash provienen de un script de Ruby que usa 'to_json'.) –

+0

" los mismos atributos se insertan en un orden diferente ". Por favor, actualice su pregunta explicando qué quiere decir con esto. –

0

Aquí es un codificador JSON simple en Qt - debería ser relativamente fácil de refundición en Java. Todo lo que necesita hacer es asegurarse de que las claves estén ordenadas al escribir: puede leer con otro paquete JSON.

QString QvJson::encodeJson(const QVariant& jsonObject) { 
    QVariant::Type type = jsonObject.type(); 
    switch (type) { 
     case QVariant::Map: 
      return encodeObject(jsonObject); 
     case QVariant::List: 
      return encodeArray(jsonObject); 
     case QVariant::String: 
      return encodeString(jsonObject); 
     case QVariant::Int: 
     case QVariant::Double: 
      return encodeNumeric(jsonObject); 
     case QVariant::Bool: 
      return encodeBool(jsonObject); 
     case QVariant::Invalid: 
      return encodeNull(jsonObject); 
     default: 
      return encodingError("encodeJson", jsonObject, ErrorUnrecognizedObject); 
    } 
} 

QString QvJson::encodeObject(const QVariant& jsonObject) { 
    QString result("{ "); 
    QMap<QString, QVariant> map = jsonObject.toMap(); 
    QMapIterator<QString, QVariant> i(map); 
    while (i.hasNext()) { 
     i.next(); 
     result.append(encodeString(i.key())); 

     result.append(" : "); 

     result.append(encodeJson(i.value())); 

     if (i.hasNext()) { 
      result.append(", "); 
     } 
    } 
    result.append(" }"); 
    return result; 
} 

QString QvJson::encodeArray(const QVariant& jsonObject) { 
    QString result("[ "); 
    QList<QVariant> list = jsonObject.toList(); 
    for (int i = 0; i < list.count(); i++) { 
     result.append(encodeJson(list.at(i))); 
     if (i+1 < list.count()) { 
      result.append(", "); 
     } 
    } 
    result.append(" ]"); 
    return result; 
} 

QString QvJson::encodeString(const QVariant &jsonObject) { 
    return encodeString(jsonObject.toString()); 
} 

QString QvJson::encodeString(const QString& value) { 
    QString result = "\""; 
    for (int i = 0; i < value.count(); i++) { 
     ushort chr = value.at(i).unicode(); 
     if (chr < 32) { 
      switch (chr) { 
       case '\b': 
        result.append("\\b"); 
        break; 
       case '\f': 
        result.append("\\f"); 
        break; 
       case '\n': 
        result.append("\\n"); 
        break; 
       case '\r': 
        result.append("\\r"); 
        break; 
       case '\t': 
        result.append("\\t"); 
        break; 
       default: 
        result.append("\\u"); 
        result.append(QString::number(chr, 16).rightJustified(4, '0')); 
      } // End switch 
     } 
     else if (chr > 255) { 
      result.append("\\u"); 
      result.append(QString::number(chr, 16).rightJustified(4, '0')); 
     } 
     else { 
      result.append(value.at(i)); 
     } 
    } 
    result.append('"'); 
    QString displayResult = result; // For debug, since "result" often doesn't show 
    Q_UNUSED(displayResult); 
    return result; 
} 

QString QvJson::encodeNumeric(const QVariant& jsonObject) { 
    return jsonObject.toString(); 
} 

QString QvJson::encodeBool(const QVariant& jsonObject) { 
    return jsonObject.toString(); 
} 

QString QvJson::encodeNull(const QVariant& jsonObject) { 
    return "null"; 
} 

QString QvJson::encodingError(const QString& method, const QVariant& jsonObject, Error error) { 
    QString text; 
    switch (error) { 
     case ErrorUnrecognizedObject: 
      text = QObject::tr("Unrecognized object type"); 
      break; 
    default: 
      Q_ASSERT(false); 
    } 
    return QObject::tr("*** Error %1 in QvJson::%2 -- %3").arg(error).arg(method).arg(text); 
} 
0

Ordene las teclas de los objetos que está serializando antes de darles salida. En Ruby 1.9 hashes están ordenados por defecto; en Ruby 1.8 no lo son. Puedes usar OrderedHash desde active_support para estar seguro en cualquier caso.

Cuando vaya a escribir sus datos JSON, ordene las claves. Tenga en cuenta que en Ruby 1.8, los símbolos no se pueden ordenar, por lo que debe llamar al to_s en su ordenación.

require 'rubygems' 
require 'json' 
require 'active_support/ordered_hash' 

obj = { 
    :fig => false, 
    :bananas => false, 
    :apples => true, 
    :eggplant => true, 
    :cantaloupe => true, 
    :dragonfruit => false 
} 

def sorted_hash(hsh) 
    sorted_keys = hsh.keys.sort_by { |k| k.to_s } 
    sorted_keys.inject(ActiveSupport::OrderedHash.new) do |o_hsh, k| 
    o_hsh[k] = hsh[k] 
    o_hsh 
    end 
end 

puts JSON.pretty_generate(obj) 
# Could output in any order, depending on version of Ruby 
# { 
# "eggplant": true, 
# "cantaloupe": true, 
# "dragonfruit": false, 
# "fig": false, 
# "bananas": false, 
# "apples": true 
# } 

puts JSON.pretty_generate(sorted_hash(obj)) 
# Always output in the same order 
# { 
# "apples": true, 
# "bananas": false, 
# "cantaloupe": true, 
# "dragonfruit": false, 
# "eggplant": true, 
# "fig": false 
# } 

Si los datos se compone de una matriz de objetos u objetos anidados, necesitará crear hashes ordenados de forma recursiva:

nested_obj = {:a => {:d => true, :b => false}, :e => {:k => false, :f => true}, :c => {:z => false, :o => true}} 

def recursive_sorted_hash(hsh) 
    sorted_keys = hsh.keys.sort_by { |k| k.to_s } 
    sorted_keys.inject(ActiveSupport::OrderedHash.new) do |o_hsh, k| 
    o_hsh[k] = hsh[k].is_a?(Hash) ? recursive_sorted_hash(hsh[k]) : hsh[k] 
    o_hsh 
    end 
end 

puts JSON.pretty_generate(nested_obj) 
# Again, could be in any order 
# { 
# "a": { 
#  "b": false, 
#  "d": true 
# }, 
# "e": { 
#  "f": true, 
#  "k": false 
# }, 
# "c": { 
#  "z": false, 
#  "o": true 
# } 
# } 

puts JSON.pretty_generate(recursive_sorted_hash(nested_obj)) 
# Even nested hashes are in alphabetical order 
# { 
# "a": { 
#  "b": false, 
#  "d": true 
# }, 
# "c": { 
#  "o": true, 
#  "z": false 
# }, 
# "e": { 
#  "f": true, 
#  "k": false 
# } 
# } 
3

Si está dispuesto a pasar un poco de sobrecarga llamando al

gson.toJson(canonicalize(gson.toJsonTree(obj))); 

entonces usted puede hacer algo como esto:

protected static JsonElement canonicalize(JsonElement src) { 
    if (src instanceof JsonArray) { 
    // Canonicalize each element of the array 
    JsonArray srcArray = (JsonArray)src; 
    JsonArray result = new JsonArray(); 
    for (int i = 0; i < srcArray.size(); i++) { 
     result.add(canonicalize(srcArray.get(i))); 
    } 
    return result; 
    } else if (src instanceof JsonObject) { 
    // Sort the attributes by name, and the canonicalize each element of the object 
    JsonObject srcObject = (JsonObject)src; 
    JsonObject result = new JsonObject(); 
    TreeSet<String> attributes = new TreeSet<>(); 
    for (Map.Entry<String, JsonElement> entry : srcObject.entrySet()) { 
     attributes.add(entry.getKey()); 
    } 
    for (String attribute : attributes) { 
     result.add(attribute, canonicalize(srcObject.get(attribute))); 
    } 
    return result; 
    } else { 
    return src; 
    } 
} 
Cuestiones relacionadas