What would be a more efficient way for my object type decoder?

Currently I am working on a class decoder, which converts strings into actual objects (e.g “Vector3.new(1, 5, 3)” → Vector3.new(1, 5, 3) etc)
It supports most classes and objects (don’t really care for the types it doesn’t support), but I’m unsure about how efficient and clean my code is, especially with the booleans and numbers (perhaps somehow put them into 1 check?).

I am not using loadstring for several reasons (mostly security).

My current code;

local function decodeType(o, toFindClass)
	if o == 'false' then return false; end;
	if o == 'true' then return true; end;
	if tonumber(o) then return tonumber(o); end;
	local startIndex, endIndex = o:find'.-%.new%(';
	if startIndex and startIndex == 1 then
		local classType = o:sub(1, endIndex - 5);
		if encodeTypes[classType] then
			return getfenv()[classType].new(unpack(o:match('%b()'):sub(2, -2):split(', ')));
		else
			error('Unsupported type for decodeType: ' .. tostring(classType));
		end;
	end;
    -- if its a string
	return tostring(o):sub(2, -2);
end;

First of all, congrats on not using loadstring!

If you’re going to be serializing objects, I’m concerned about the maintainability that comes with storing values as their source-code constructs. I highly recommend you use JSON instead.

For instance, the (verbose) implementation for serializing and deserializing Vector3 values would look like this:

local HttpService = game:GetService("HttpService");

local function vector3ToObject(vector3)
	local object = {
		X = vector3.X,
		Y = vector3.Y,
		Z = vector3.Z,
	};
	return object;
end

local function serializeVector3(vector3)
	local object = vector3ToObject(vector3);
    local json = HttpService:JSONEncode(object)
	return json;
end

local function reviveVector3(object)
	local vector3 = Vector3.new(object.X, object.Y, object.Z);
	return vector3;
end

local function deserializeVector3(json)
	local object = HttpService:JSONDecode(json);
	local vector3 = reviveVector3(object);
	return vector3;
end

What you’re doing right now is “magic code” and is, for the most part, not a good practice. However, I did de-magicfy your code a little:

local HttpService = game:GetService("HttpService");
 
local CLASS_NAME_PATTERN = "(%w+).new%((.+)%)";
local ARGUMENT_PATTERN = "%w";

--- Decodes the given value
--- NOTE: This will error if the serialized value is not
--- valid JSON or is a stringified JSON array / table.
--- @param serializedValue string
local function decodePrimitive(serializedValue)
	local primitive = HttpService:JSONDecode(serializedValue);
	return primitive;
end

--- Decodes the given source
--- @param source string
local function decodeSource(source)
	-- This won't error if the source was just a serialized
	-- primitive.
	local isPrimitive, primitive = pcall(decodePrimitive, source);
	
	if isPrimitive then
		return primitive;
	end
	
	local className, argumentSource = source:match(CLASS_NAME_PATTERN);
	
	if not className then
		local message = ("malformed source %q"):format(source);
		error(message, 2);
	end
	
	local arguments = {};
	for serializedArgument in argumentSource:gmatch(ARGUMENT_PATTERN) do
		-- We should only have primitives at this point, so
		-- we should NOT suppress errors here.
		local value = decodePrimitive(serializedArgument)
		table.insert(arguments, value);
	end
	
	local class = getfenv(0)[className];
	if not class then
		local message = ("invalid class name %q"):format(className);
		error(message, 2);
	end
	return class.new(unpack(arguments));
end

And for this, I ran a few quick tests:

local serializedString = '"Hello, world!"';
local serializedBoolean = "true";
local serializedVector3 = "Vector3.new(1, 1, 1)";

print(decodeSource(serializedString));
print(decodeSource(serializedBoolean));
print(decodeSource(serializedVector3));

Hello, world!
true
1, 1, 1

If you have any more questions, feel free to ask!

1 Like

Don’t really see the point in doing all this encoding when something as simple as

('%s.new(%s)'):format(typeof(Class), tostring(Class));

would suffice.

I currently just do

if encodeTypes[Class] then 
    return encodeTypes[Class]:format(tostring(o)); 
end;

I suppose the main reason other developers on this platform have dedicated time to making some serialization library and why, in real-world scalable applications, they serialize into a medium like JSON is because it’s standardized—but, more importantly, it’s compressed.

Coming from a background of package management and dealing with highly scalable applications, I’ve learned that magic code like this is actually quite lack-luster. Although I truly advise against it, your current solution isn’t doing anything that immediately seems like an issue and what few issues I see have such a low chance of happening that it’s negligible.

I do see your point here and it would be a good idea to do as such, but I probably won’t be switching from lua over the next few years, so I’m fine on that part.

1 Like