ZJ: The tiny JSON encoder/decoder

Copyright © 2018 Craig Everett

Version: 1.0.1

Authors: Craig Everett (ceverett@zxq9.com) [web site: https://gitlab.com/zxq9/zj].

This is a single-module library that encodes and decodes JSON. The JSON definition followed is RFC 8259.

Functions

This library exposes four functions: encode/1, decode/1, binary_encode/1, binary_decode/1.

encode/1

Crashes on data that it cannot handle. The kinds of data encode/1 cannot handle are:

encode/1 does not accept magical values such as tagged tuple indicators to distinguish between types (for example: lists-as-strings and lists-as-lists). For the vast majority of encoding uses encode/1 is the right option. If you require lists of integer values to always be encoded as arrays (not strings) and are willing to convert all internal io_list data to binary UTF-8 strings before calling an encoding function, then binary_encode/1 is for you.

decode/1

Returns a success/fail tuple: - {ok, Value} on success - {error, Partial, Remaining} on failure - {incomplete, Partial, Remaining} from unicode:characters_to_list/1 on illegal unicode

No annotated or magical JSON types are anticipated by this function. It is very unlikely that any will be added in the future.

binary_encode/1

Very similar to encode/1 (including crashing on bad input values), except for the following differences:

binary_decode/1

Very similar to decode/1, except for the following differences:

Examples (in the shell)

    1> zj:encode(1492).
    "1492"
    2> zj:decode("1492").
    {ok,1492}
    3> zj:decode("14.242e-21").
    {ok,1.4242e-20}
    4> zj:encode([1, 2, 3, 4]).
    "[1,2,3,4]"
    5> zj:decode("[1, 2, 3, 4]").
    {ok,[1,2,3,4]}
    6> zj:encode("Hello").
    "\"Hello\""
    7> zj:encode(<<"Hello">>).
    "\"Hello\""
    8> zj:encode(some_atom).  
    "\"some_atom\""
    9> zj:encode(#{some => ["set", "of"], "data" => ["that you", <<"want encoded">>]}).
    "{\"data\":[\"that you\",\"want encoded\"],\"some\":[\"set\",\"of\"]}"
    10> zj:decode("{\"data\":[\"that you\",\"want encoded\"],\"some\":[\"set\",\"of\"]}").
    {ok,#{"data" => ["that you","want encoded"],
          "some" => ["set","of"]}}
    11> zj:decode("[\"A map with an illegal key\", {42:\"boom!\"}").
    {error,["A map with an illegal key",#{}],"42:\"boom!\"}"}
    12> zj:decode("An illegally unquoted string").
    {error,[],"An illegally unquoted string"}
    13> zj:decode("\"An illegal, lonely quote mark\" \"").        
    {error,"An illegal, lonely quote mark","\""}

Type Mapping

Types don't match well between Erlang and JSON. There are tradeoffs involved in any mapping. Note that the mapping for the function binary_encode/1 is slightly different (and requires a little bit more work to use) but provides a completely unambiguous way to generate lists of integer values VS unicode strings.

encode/1 (Erlang -> JSON)

  Integer   -> Integer
  Float     -> Float
  Map       -> Object
  List      -> Array
  Tuple     -> Array
  Binary    -> String
  UTF-8     -> String
  true      -> true
  false     -> false
  undefined -> null
  Atom      -> String

decode/1 (JSON -> Erlang)

  Integer -> Integer
  Float   -> Float
  String  -> String
  Object  -> Map
  Array   -> List
  true    -> true
  false   -> false
  null    -> undefined

binary_encode/1 (Erlang -> JSON)

  Integer   -> Integer
  Float     -> Float
  Map       -> Object
  List      -> Array
  Tuple     -> Array
  Binary    -> String
  true      -> true
  false     -> false
  undefined -> null
  Atom      -> String

binary_decode/1 (JSON -> Erlang)

  Integer -> Integer
  Float   -> Float
  String  -> Binary
  Object  -> Map
  Array   -> List
  true    -> true
  false   -> false
  null    -> undefined

Rationale

This library scratches a few itches of mine.

Six needs

  1. I need to be able to read really basic JSON (This means RFC-8259, not everything allowable as per the lolscript standard)
  2. The data might contain complex utf-8 character constructions
  3. Must work on Windows (client-side) with only zx + Erlang installed
  4. Mochi is based on obsolete "tuple calls" in Erlang, and support finally died R21
  5. String output needs to be actual strings
  6. I like tiny, targeted projects where problems are obvious

Six 'meh' factors

  1. lolspeed is not a requirement
  2. JSON objects are represented best as Erlang maps
  3. The memory footprint of strings is not a concern
  4. Most outrageous JSON "gotcha" cases don't apply to my data
  5. Letting Erlang chomp large precision floats or accept bignums is OK
  6. Strings nested in things has worked without magical type specifiers

On the note of strings, the function binary_encode/1 was added specifically to provide a way to disambiguate between lists and strings when encoding, but in the most common use cases this is not necessary. The inverse, binary_decode/1 serves two purposes that are similarly not of paramount importance in most cases: reducing the memory footprint of Erlang strings and (more importantly) disambiguating between JSON integer arrays and strings (but srsly if you have no clue what data you're receiving, then...?).

Contributing

If you find this library useful and would like to see a feature added, please contact the author via email (see author line in source), IRC, or Erlang's Slack channel (ugh).

If you want to turn this into a 10k LoC project, consider using JSX instead.

Generated by EDoc