PiTyUs.Hire me

Getting started

Lua tables for FiveM developers

Everything about Lua tables for FiveM scripts: arrays vs dictionaries, ipairs vs pairs, table.insert/remove/sort/concat, nested config tables, copying (references), sparse-array bugs and converting to JSON.

Updated 13 min readBy PiTyUs · FiveM developer

Overview

Tables are the only data structure Lua has, and FiveM scripts are made of them: configs, job lists, item definitions, player caches, NUI payloads. Most confusing bugs in beginner scripts — a loop that stops early, a config that changes by itself, a count that is wrong — are table bugs. This guide covers the patterns that come up constantly and the traps behind those bugs.

Arrays and dictionaries

The two shapeslua
-- An array: values with implicit keys 1, 2, 3
local jobs = { 'police', 'ambulance', 'mechanic' }
print(jobs[1])      -- police
print(#jobs)        -- 3

-- A dictionary: explicit keys
local prices = { bread = 5, water = 2 }
print(prices.bread)       -- 5
print(prices['water'])    -- 2

-- Both, and nesting
local shop = {
    label = '24/7',
    coords = vector3(25.7, -1347.3, 29.5),
    items = {
        { name = 'bread', price = 5 },
        { name = 'water', price = 2 },
    },
}
print(shop.items[2].name)  -- water

Looping: ipairs vs pairs

`ipairs(t)``pairs(t)`
VisitsKeys 1, 2, 3… until the first nilEvery key
OrderIn orderNot guaranteed
Use forArraysDictionaries
Loopinglua
for i, job in ipairs(jobs) do
    print(i, job)
end

for item, price in pairs(prices) do
    print(item, price)
end

-- Plain numeric loop, often the fastest for arrays
for i = 1, #jobs do
    print(jobs[i])
end

The table library

table.*lua
local list = { 'b', 'c' }

table.insert(list, 'd')        -- append: b c d
table.insert(list, 1, 'a')     -- insert at 1: a b c d
local removed = table.remove(list) -- remove last, returns 'd'
table.remove(list, 1)          -- remove first

list[#list + 1] = 'e'          -- append without a function call

table.sort(list)               -- alphabetical
table.sort(shop.items, function(a, b) return a.price < b.price end)

print(table.concat(list, ', ')) -- join into a string

The length operator and gaps

#t gives the length of the array part — but only when there are no nil holes. Setting an element in the middle to nil creates a gap, and #, ipairs and table.concat may stop at it or return confusing results.

The gap buglua
local players = { 'A', 'B', 'C' }
players[2] = nil            -- hole in the middle
print(#players)             -- may be 3 or 1 — undefined
for _, p in ipairs(players) do print(p) end  -- prints only A

-- Correct: remove, which shifts the rest down
local players2 = { 'A', 'B', 'C' }
table.remove(players2, 2)   -- { 'A', 'C' }

For collections keyed by player ID — which are naturally sparse — use a dictionary and count with a loop or a separate counter, never #.

Removing while looping

Loop backwards when removinglua
-- Wrong: forward loop skips the element after each removal
-- Right: walk from the end
for i = #vehicles, 1, -1 do
    if not DoesEntityExist(vehicles[i]) then
        table.remove(vehicles, i)
    end
end

Tables are references

Assigning a table to another variable does not copy it; both names point to the same table. This is how a config “changes by itself”: a script modifies what it thinks is its own copy.

Copyinglua
local a = { money = 100 }
local b = a
b.money = 0
print(a.money) -- 0 — same table

local function deepCopy(t)
    if type(t) ~= 'table' then return t end
    local out = {}
    for k, v in pairs(t) do out[k] = deepCopy(v) end
    return out
end

local c = deepCopy(a) -- an independent copy

The same applies across events: a table sent with an event arrives as a new copy on the other side, because it is serialised. Changing it there does not change the sender’s table.

Fast lookups instead of searches

A setlua
-- Slow if called often: scans the array each time
local allowed = { 'police', 'sheriff', 'fib' }

-- Fast: one lookup
local ALLOWED = { police = true, sheriff = true, fib = true }
if ALLOWED[job] then
    -- ...
end

Turning “is X in this list?” into a key lookup is one of the simplest Lua performance wins.

Tables and JSON

json.encode / json.decodelua
local data = { name = 'Mike', items = { 'phone', 'id_card' } }
local text = json.encode(data)   -- '{"items":["phone","id_card"],"name":"Mike"}'
local back = json.decode(text)
print(back.items[1])             -- phone
  • JSON is how tables are stored in a database text column, sent to NUI and exchanged with web APIs.
  • Arrays with gaps and tables mixing numeric and string keys encode unpredictably — keep data clean.
  • json.decode returns nil for invalid JSON; check before using the result.

Frequently asked questions

What is a table in Lua?

Lua’s only data structure. The same type serves as an array, a dictionary, an object or a mix of all three.

Why does #table return the wrong number?

The table has a nil gap, or it is a dictionary. # only counts the array part up to the first gap.

How do I copy a table in Lua?

Assignment copies only the reference. Write a small deep-copy function that builds a new table and copies each value, recursing into nested tables.

How do I add an item to a Lua array?

table.insert(list, value) or list[#list + 1] = value.

How do I convert a Lua table to JSON in FiveM?

json.encode(t) returns a JSON string and json.decode(s) turns it back into a table.

Need this built, not just explained?

Ten years of FiveM work, from Lua to NUI

Custom resources, React NUI, ESX / QBCore / Qbox integration, OneSync performance audits and security reviews — plus the websites and SEO around your server brand.

Related guides