I wrote a neat thing that traverses the given table and will return if it’s valid or not. If invalid, it returns the path to the invalid key/value, the reason, and any extra info.
I’ve included test cases. It should be easy to remove those and use this to your advantage by looking at the test code as an example.
local function typeValid(data)
return type(data) ~= 'userdata', typeof(data)
end
local function scanValidity(tbl, passed, path)
if type(tbl) ~= 'table' then
return scanValidity({input = tbl}, {}, {})
end
passed, path = passed or {}, path or {'input'}
passed[tbl] = true
local tblType
do
local key, value = next(tbl)
if type(key) == 'number' then
tblType = 'Array'
else
tblType = 'Dictionary'
end
end
local last = 0
for key, value in next, tbl do
path[#path + 1] = tostring(key)
if type(key) == 'number' then
if tblType == 'Dictionary' then
return false, path, 'Mixed Array/Dictionary'
elseif key%1 ~= 0 then -- if not an integer
return false, path, 'Non-integer index'
elseif key == math.huge or key == -math.huge then
return false, path, '(-)Infinity index'
end
elseif type(key) ~= 'string' then
return false, path, 'Non-string key', typeof(key)
elseif tblType == 'Array' then
return false, path, 'Mixed Array/Dictionary'
end
if tblType == 'Array' then
if last ~= key - 1 then
return false, path, 'Array with non-sequential indexes'
end
last = key
end
local isTypeValid, valueType = typeValid(value)
if not isTypeValid then
return false, path, 'Invalid type', valueType
end
if type(value) == 'table' then
if passed[value] then
return false, path, 'Cyclic'
end
local isValid, keyPath, reason, extra = scanValidity(value, passed, path)
if not isValid then
return isValid, keyPath, reason, extra
end
end
path[#path] = nil
end
passed[tbl] = nil
return true
end
local function getStringPath(path)
return table.concat(path, '.')
end
local function warnIfInvalid(input)
local isValid, keyPath, reason, extra = scanValidity(input)
if not isValid then
if extra then
warn('Invalid at '..getStringPath(keyPath)..' because: '..reason..' ('..tostring(extra)..')')
else
warn('Invalid at '..getStringPath(keyPath)..' because: '..reason)
end
else
print('Valid')
end
end
---
local cyclicTest = {
a = {{b = {}}}
}
cyclicTest.a[1].b[1] = cyclicTest
local testCases = {
true, 'hello', 5, 5.7, -- all valid
CFrame.new(), -- invalid: type
{
true, 'hello', 5, 5.7
}, -- valid array
{
a = true, b = 'hello', c = 5, d = 5.7
}, -- valid dictionary
{
a = true, 'hello', 5, 5.7
}, -- invalid: array/dictionary mix
{
CFrame.new()
}, -- invalid: type in array
{
in1 = {
{
in2 = {
a = true, 'hello'
}
},
5
},
in3 = {}
}, -- invalid: array/dictionary mix deep in path
{
[5.7] = 'hello'
}, -- invalid: decimal index
{
[{}] = 'hello'
}, -- invalid: non-string key
{
[1] = 'hello',
[3] = 'WRONG',
}, -- invalid: non-sequential array
cyclicTest, -- invalid: cyclic
{
[math.huge] = 'hello'
}, -- invalid: infinity index
}
for _, case in next, testCases do
warnIfInvalid(case)
end
Non-sequential arrays are technically valid, but everything after the sequence breaks will be cut off and you will lose data. It’s better to recognize that as invalid data than to ignore it.