JSON, Unicode und UTF-8

Als ich vor ein paar Tagen mit der API.Leipzig rumspielen wollte, startete ich mit dem RestClient Gem ein paar Anfragen und stieß auf ein Anfangs komisches Problem:

RestClient.get 'http://www.apileipzig.de/api/v1/district/streets', :params =>{:api_key => '...', :limit => 1}

Dieser Befehl lieferte mir folgendes zurück:

{
    "data": [
        {
            "housenumber": 1,
            "name": "Abrahamstra\u00dfe",
            "created_at": "2011-05-23T21:41:04+02:00",
            "postcode": "04179",
            "district_id": 50,
            "updated_at": null,
            "id": 1,
            "housenumber_additional": "A",
            "internal_key": "06143"
        }
    ]
}

Statt einem 'ß' in der Abrahamstraße, also Unicode. Das sah auf den ersten Blick nach einem Fehler beim Encoding aus und ich habe eine Weile gebraucht um das Problem zu lösen. Unter anderem findet sich hier die Lösung:

RestClient funktioniert korrekt. Die API funktioniert ebenfalls korrekt, auch ein lokal aufgesetztes API System lieferte, mit unterschiedlichen Konfigurationen, immer das gleiche Ergebnis. Die Zeile, die in der API schlussendlich eine Ausgabe erzeugt ist

# API Leipzig's JSON Ausgabe https://github.com/apileipzig/api/blob/master/lib/helpers.rb#L124
output.to_json

output ist ein Hash und to_json stammt aus dem JSON Gem, das diesen Hash in JSON umwandelt. Bei dieser Umwandlung werden UTF-8 Zeichen, die nicht im ASCII Zeichensatz vorkommen, in Unicode codepoints umgewandelt. Das machen die meisten JSON Bibliotheken um auch kompatibel zu Empfängern zu sein, die kein UTF-8 empfangen können. Als ich dann ins JSON RFC geschaut hatte, wurde mir einiges klar:

If the character is in the Basic Multilingual Plane (U+0000 through U+FFFF), then it may be represented as a six-character sequence: a reverse solidus, followed by the lowercase letter u, followed by four hexadecimal digits that encode the character's code point. The hexadecimal letters A though F can be upper or lowercase. So, for example, a string containing only a single reverse solidus character may be represented as "\u005C".

JSON RFC Punkt 2.5 http://www.ietf.org/rfc/rfc4627.txt

Weiterlesen im RFC...

JSON text SHALL be encoded in Unicode. The default encoding is UTF-8.

JSON RFC Punkt 3 http://www.ietf.org/rfc/rfc4627.txt

Alle neueren Browser berücksichtigen das und wandeln automatisch bei der Anzeige von reinem JSON in UTF-8. Das klärt auch die Frage, warum Chromium und Firefox alles korrekt anzeigen, während mein eigenes kleines Script Probleme machte. Wer aber keine JSON Bibliothek benutzen möchte, kann aus dem Unicode wieder reines UTF-8 machen:

# Lösung http://stackoverflow.com/questions/5123993/json-encoding-wrongly-escaped-rails-3-ruby-1-9-2
response = RestClient.get 'http://www.apileipzig.de/api/v1/district/streets', :params =>{:api_key => '...', :limit => 1}
puts response.gsub!(/\\u([0-9a-z]{4})/) {|s| [$1.to_i(16)].pack("U")}

Nun hat die Abrahamstraße wieder ihr 'ß' und ich kann weiterarbeiten. Gut zu Wissen! :)