Using JSON With Lua

We’ve worked with basic file operations and covered the basics of JSON as well. Let’s learn how to put them together.

There are many JSON libraries available for Lua, but I true to stick with pure Lua libraries unless I really need the extra performance. If you notice your application running slow or similar, you may look for C libraries with bindings in Lua. For this lesson, we are going to cover using Lunajson.

We’re going to go over installing Lunajson first, including the use of Luarocks. We’ll then cover using Lunajson to working with both decoding and encoding. Finally, we’ll go over some more applicable use cases for JSON.

Luarocks

Luarocks is basically a package manager for Lua libraries. It allows you to install packages and compile them without having to keep track of the individual pieces yourself. If you use libraries which are not pure Lua, this tool is indispensable. I use pure Lua where possible for portability.

If you are running something like Debian or Ubuntu, you just have to use apt to install it.

sudo apt install luarocks

The directions are a bit more complicated for Windows and you should follow the guide from Luarocks. The process is relatively easy, but has a few extra steps. When I work with Windows for Lua, I set up a base machine, build my libraries, and then port them over.

Mac OS X or MacOS requires something like Homebrew or some other third-party repository. You’ll run something like:

brew update
brew install luarocks

You can see this guide for more.

Installing Luajson

If you have Luarocks installed, you just need to run the following.
Linux or Mac OS X:

sudo luarocks install lunajson

Windows:

luarocks install lunajson

If you don’t want to use Luarocks, or are unable to, you can download Lunajson. Download everything in the src folder. Keep the subfolder named the same way as well. You don’t need the rest to get going with Lunajson.

To use these, either place them in your library path, or else put them in the folder you will be using your program from. For Debian and Ubuntu, the default path will be something like /usr/local/share/lua/5.x/ where the x is going to be the Lua version. This gets far more complicated on Windows and MacOS due to the fact that the installation method is a bit less consistent.

Decoding JSON

Here’s a basic example of a trivial JSON decoding program:

#!/usr/bin/lua5.1

lunajson = require 'lunajson'

local jsonraw = '{"test":[1,2,3,4]}'
local jsonparse = lunajson.decode( jsonraw )

print( jsonparse["test"][ 1 ] .. ", " .. jsonparse["test"][ 2 ] .. ", " .. jsonparse["test"][ 3 ] .. ", " .. jsonparse["test"][ 4 ] )

This outputs:

./json1.lua 
1, 2, 3, 4

Let’s go over what we’re doing with this program. We set a variable for our object, lunajson and require our library. We then use the decode method to decode the JSON and assign it to our variable.

As you can see from our work with tables, the decode method returns a table with our data. We access this table as a series of tables and arrays depending on how the JSON is formed. By only using these two data structures, JSON stays extremely easy to work with in Lua. It also allows data structures to be reproducible consistently between programs and languages.

Lunajson’s decode function has some cool other features. The full function looks like follows:

lunajson.decode( [JSON string] [, [position] [, [null value] [, [array length] ] ] ] )
JSON stringThe scalar string which is a representation of your JSON.
positionThe position to start with in the string. Useful for JSON with a header (e.g. “results={…}” some APIs return)
null valueWhat arbitrary value to replace JSON null with.
array lengthA boolean (true or false) for whether to set the 0 element of an array to the length to help differentiate empty arrays from nil objects.

Let’s see these options in action:

#!/usr/bin/lua5.1

lunajson = require 'lunajson'

local jsonraw = 'junk{"test":[1,2,3,4],"answer":null}'
local jsonparse = lunajson.decode( jsonraw, 5, "--junk--", true )

print( "Length of \"test\" item: " .. jsonparse["test"][0] )
print( jsonparse["test"][ 1 ] .. ", " .. jsonparse["test"][ 2 ] .. ", " .. jsonparse["test"][ 3 ] .. ", " .. jsonparse["test"][ 4 ] )
print( jsonparse["answer"] )

This should get you the following results:

./json2.lua 
Length of "test" item: 4
1, 2, 3, 4
--junk--

Encoding

Let’s look at a trivial example encoding a Lua data structure into JSON.

#!/usr/bin/lua5.1

luna = require 'lunajson'

local test = { ["cat"] = { ["name"] = "MeowMeow", ["age"] = 5 }, ["dog"] = { ["name"] = "Good Boy", ["age"] = 12 } }

local json = luna.encode( test )

print( json )

This gets us:

./json3.lua
{"cat":{"name":"MeowMeow","age":5},"dog":{"name":"Good Boy","age":12}}

The encode function has an extra parameter like our decode function did.

lunajson.encode( [Lua table] [, [null value] ] )
Lua tableThe Lua table which will be turned into JSON.
null valueWhat arbitrary value to turn into JSON null.

Working With Files

We worked with files last lesson to get ready to deal with creating persistent storage and to be able to access data which is not attached to our program. This also means we can read and write JSON files which can help for storing configurations and similar. You can use flat files and all sorts of other methodologies, but this comes in handy when working with something which already uses JSON.

Let’s see a simple example:

#!/usr/bin/lua5.1

luna = require 'lunajson'

local data = {}

io.write( "Please enter a file to use: " )
local file = io.read()

print( "Type 'quit' to quit!" )

repeat
	io.write( "Type a value to name: " )
	local input = io.read()
	
	if input ~= "quit" then
		io.write( "Type a value to use (--junk-- is null): " )
		local value = io.read()
		
		data[input] = value
	end
until input == "quit"

local fh = io.open( file, "w" )

if io.type( fh ) == "file" then
	fh:write( luna.encode( data, "--junk--" ) .. "\n" )
	
	fh:close()
else
	print( "ERROR: Unable to open: " .. file )
end

If we run through it, we should get something like follows:

./jsonfile1.lua 
Please enter a file to use: json.out
Type 'quit' to quit!
Type a value to name: dog
Type a value to use (--junk-- is null): Spot
Type a value to name: cat
Type a value to use (--junk-- is null): Frisky
Type a value to name: bird
Type a value to use (--junk-- is null): --junk--
Type a value to name: quit

And if we open json.out we’ll see something like:

{"bird":null,"dog":"Spot","cat":"Frisky"}

Other Functions In Lunajson

Lunajson also has the ability to function as a SAX parser. SAX stands for Simple API for XML. Basically, when we read the whole JSON file in, we’re putting it all in memory for how we’ve done things before. SAX is useful for massive files, but typically adds too much complexity for the average user. It also slows down execution.

Here’s a basic example in Lunajson:

#!/usr/bin/lua5.1

local luna = require 'lunajson'
local file = "json.out"
local saxtbl = { ["key"] = function (s) print( s ) end, ["startobject"] = function () print( "Found an object!" ) end }

local parser = luna.newfileparser( file, saxtbl )

parser.run()

This shows us:

./lunasax1.lua 
Found an object!
bird
dog
cat

See the documentation for more information about what is available and what you can do with Lunajson’s SAX implementation. SAX takes a good bit of getting used to and a very specific purpose which is not worth covering at this level.

Exercises

Exercise 1

Take the JSON file program:

#!/usr/bin/lua5.1

luna = require 'lunajson'

local data = {}

io.write( "Please enter a file to use: " )
local file = io.read()

print( "Type 'quit' to quit!" )

repeat
	io.write( "Type a value to name: " )
	local input = io.read()
	
	if input ~= "quit" then
		io.write( "Type a value to use (--junk-- is null): " )
		local value = io.read()
		
		data[input] = value
	end
until input == "quit"

local fh = io.open( file, "w" )

if io.type( fh ) == "file" then
	fh:write( luna.encode( data, "--junk--" ) .. "\n" )
	
	fh:close()
else
	print( "ERROR: Unable to open: " .. file )
end

Make it read the old file and display it as a set of keys and values. See Bringing More to the Table with Lua (Part 2) for hints. Your end program should display something like:

Please enter a file to use: json.out
----json.out----
bird=nil
dog=Spot
cat=Frisky
----EoF----
Exercise 2

Rewrite the program Exercise 1 is based on, but allow handling for arrays.

Exercise 3

Write a program which combines Exercise 1 and Exercise 2 with a control structure. You should be able to navigate from a text menu and either “read”, “write”, or “exit”.

Optional Exercise

Write a program using Lunajson SAX. Have it return a similar output as Exercise 1.