How to save parts, and the idea of Serialization

This article is the answer to the famous question “How to save objects using Datastore” and many other ways to word it.


If you played with Datastore for enough time, I think you already know that Instances such as parts and models ect. can’t be simply saved using :SetAsync(), which makes it hard to save objects. Most people, with games that are supposed to have slots and stuff on them that you want to save, get stuck when doing this, although, I think finding a way to go over this is quite easy.

To keep it short, a way to store objects, is to convert them into dictionaries, where each key is a property of that object, and each value is the value of that corresponding property.

Meaning, if I had a part named "Bob", with Transparency set to 0.5, and Anchored to true, we would convert it into something like this:

{Name = "Bob", Transparency = 0.5, Anchored = true}

sample1

This my friend, is called serialization.

By definition, serialization is taking a piece of data that can’t be saved or transfered through the network, and turning it into a piece of data that can be saved or transfered through the network.
Like in our case, a part can’t be saved, which is why we serialized it so we can save it. (and you can’t send instances over remote events, which is why you need to serialize them as well)

Why can’t certain pieces of data be saved or transfered? Well, that idea is meant to be abstracted away from the user, just know that some things can’t be saved and you have to serialize it. For example, if you were displaying an image, and the way you were displaying that image was by retrieving it’s address (directory or hierarchy inside the computer, like
D:/Images/WantedImage), what if you wanted to send that information over the internet? You can’t just send your computer’s hierarchy, other computers won’t have that same image in that placement, they might not even have that placement. You need to serialize that image, by converting it into an array containing each and every pixel of that image!

So let’s carry on. What if I want to load the data after saving? Meaning I want to deserialize that table. In a self explainable way, deserialization is that taking that piece of data that’s compatible with saving and sending, and turning it into its usable original form that it had before the serialization.
Since we have the ability to do object[prop] (part.Transparency is the same as part["Transparency"] for example), what we can do is: create the part, then loop through the dictionarry, where key is the current property and value is the current value it’s set to, and do part[prop] = value. This might sound complicated, don’t worry, we’re doing this later.

What if we wanted to save other properties? Such as Position or BrickColor? Well we’re gonna come across other problems. All userdatas such as Vector3s, Color3s, BrickColors, CFrames ect. can’t be saved using Datastore as well. We have to serialize those as well!

There isn’t one exact way to serialize, you just have to get creative and invent your own way to do it:

  • If I wanted to serialize a Vector3, I could turn into a dictionarry with 3 keys, an X, a Y, and a Z components. And when I deserialize, I simply do Vector3.new(t.X, t.Y, t.Z), where t is the saved dictionarry.

  • For BrickColor I can simply tostring() it, then do BrickColor.new(str) where str is the saved string.

  • CFrame? This can be done in multiple ways. Since a CFrame is made up of a cf.Position, and a cf.rightVector, cf.upVector, -cf.lookVector, we can save those in a dictionarry as well with 4 keys, and when we deserialize, we CFrame.fromMatrix(t.Pos, t.rX, t.rY, t.rZ), where t is the saved dictionarry, and t.Pos is cf.Position, t.rX is cf.rightVector, t.rY is cf.upVector and t.rZ is -cf.lookVector (-lookvector and not just lookvector). Wait a second. aren’t these 4 values Vector3s? We have to serialize, then deserialize them as well!
    There are smarter and more efficient ways to go about this, but wanted to do this because it seemed nice to understand. You could simply save {cf:GetComponents()}, and deserialize this way CFrame.new(unpack(t)) where t is that saved table.

  • Color3 might be simple, since Color3s have an .R and a .G and a .B property, we can save those in a dictionarry like Vector3s, but here is a better suggestion, using c3:ToHSV(), which converts an RGB value to an HSV value, save that returned tuple in a table t = {Color3:ToHSV()}, then to deserialize do Color3.fromHSV(unpack(t)).

  • What about properties that are set to Enum values? These ones might be tricky! The way I did it was by tostring()ing it, then since Enums have each part seperated with a . (for example Enum.Material.Plastic), I can do string.split on it where "." is the seperater, and like that I have a table containing {"Enum", "Material", "Plastic"}, then what I do is save the 2nd and 3rd index, and when I deserialize, since Enum.Something is the same as Enum["Something"], I can do Enum[t[2]][t[3]], where t is the saved table containing "Material" and "Plastic", and t[2] is "Material" and t[3] is "Plastic", meaning Enum[t[1]][t[2]] evaluates to Enum["Material"]["Plastic"] (Another smarter way is to use string.gsub and do tostring(enum):gsub(".+.%(.+).%(.+)", function(x, y) a = {x, y} end where a is a table outside).

Again! All this might sound complicated, but it will be made clearer later on in the code!

Important question: How would the saved table look? Remember that the slot that we’re saving will have many objects with different classes. Simply, it’s best for it to be a dictionarry, where each key is the class of the saved object. Each key will have a table containing all of the serialized objects with that corresponding class.

local tableToSave = {
    Part = {"here we'll put all objects with the class Part"},
    Decal = {"all objects with the class Decal here"},
    SpawnLocation = {"all objects with the class SpawnLocation"}
}

sample2

Another important question: What if an object had children? You can probably save the .Parent property, but that’s gonna be complicated. What we can do is have a key inside of each serialized object that has children, called Children, that’s set to table which will have the same layout as tableToSavesample2! It will also have keys to represent classes, and each child of that serialized object will fall under the corresponding class. The children will as well be serialized seperately, like their parent. And if a child had a child, welp same thing! Have a Children table inside it. And if its children have children, add a Childern table yet again! We’ll see how this is done later (again hehe).

Another important question: How exactly will we determine what properties are checked? If you think about it, different classes have different properties, and as well we don’t want to save all properties, just certain ones. To do that, let’s say we have a table called Properties, that will store the properties that we wanna save for each class, represented as strings containing the propertie’s name.

local Properties = {
    Part = {"Name", "Position", "Size", "Transparency", "BrickColor", "CanCollide", "CFrame", "Anchored", "Shape", "Material"}
    Decal = {"Name", "Texture", "Transparency", "Face", "Color3"}
}

sample3

As you’ll see, the usefulness of this will be made clear later on (yet again).
This table is supposed to be modifiable, if you want to save certain properties, write those properties that you want. If you want to serialize more objects, add their class. ect.

So finally! Let’s start coding this!

We’ll start with the serialization of properties, creating the function responsible for it. Then we will take care of objects. Let’s start with properties first.

This function will take the value of a property as input, and return the serialized version. We will just be implementing what we talked about earlier.

local function Serialize(prop) --prop will be the property's value type
 	local type = typeof(prop) --the type of the value
	local r --the returned value afterwards
	if type == "BrickColor" then --if it's a brickcolor
		r = tostring(prop)
	elseif type == "CFrame" then --if it's a cframe
		r = {pos = Serialize(prop.Position), rX = Serialize(prop.rightVector), rY = Serialize(prop.upVector), rZ = Serialize(-prop.lookVector)}
	elseif type == "Vector3" then --if it was a vector3, this would apply for .Position or .Size property
		r = {X = prop.X, Y = prop.Y, Z = prop.Z}
	elseif type == "Color3" then --color3
		r = {Color3.toHSV(prop)}
	elseif type == "EnumItem" then --or an enum, like .Material or .Face property
		r = {string.split(tostring(prop), ".")[2], string.split(tostring(prop), ".")[3]} 
	else --if it's a normal property, like a string or a number, return it
		r = prop
	end
	return r 
end

Great! You can test it if you want. One thing to point out, notice when dealing with CFrame, I’m using Serialize inside of Serialize, because as we said earlier, a serialized CFrame is made up of 4 Vector3s, so you have to serialize them as well. We’ll look into this more later, since this is the same thing that we’ll do with the Children table.

Now, let’s serialize objects! I’m gonna make a function called InitProp (Init short for initiate). This function will take an array of objects as input, and return an array sample2 of the serialized versions of the objects.

We’ll have a table called tableToSave sample2, which will be the saved table, that will contain all of the saved classes, and the saved objects with their corresponding saved properties, this is pretty much the first table I mentioned at the start. What we will do is, loop through the inputed array, and each time we check if a key corresponding with the current object’s class exists inside of tableToSave, if not create it inside of that table. Then, we will initiate the object’s properties. For each object, we will create a table called add that will be the object’s serialized form sample1, the one that holds the properties as keys, that we mentioned all the way at the start. The way this will work, is by getting the table from Properties sample3 corresponding with the object’s class (the properties we want to save for this class), loop through those properties, doing add[prop] = Serialize(obj[prop]) where prop is the current property from the properties tables sample3. Basically, what we’re doing is: make a key inside of the serialized object (add) table representing that property (add[prop], creating a key inside of add with the name of property), and set to the serialized version of the property (Serialize(obj[prop]), obj[prop] is the value the property is set to). Basically, we’re turning the instance, into that serialized table form sample1 we talked about from the start.

local function InitProps(objects) --objects is the array of the objects to serialize
	local tableToSave = {} --the table that will hold the serialized versions of objects
    for _, obj in pairs(objects) do
	     local class = obj.ClassName --the object's class
        
	     local t = tableToSave[class] --this is the class table containing all the saved objects with the same class
		 if not(t) then --check if that class table existed
		 	 tableToSave[class] = {} --if not, create it
		 	 t = tableToSave[class] --save a reference to it
		 end

         local add = {}  --the current serialized object that we will be filled with properties
	     for _, prop in pairs(Properties[class]) do --Prop will be the property corresponding to the class, notice how we're doing obj.ClassName, to get the properties for that wanted class, doing _ pretty much says I don't need this value it's useless, which is why I did it
		     add[prop] = Serialize(obj[prop]) --do the magic
	     end
         table.insert(t, add) --insert the magic after all properties are initiated into its class table
    end
    return tableToSave --return the magic after all objects are initiated
end

Simple right? Maybe not, which is why I advise you to take another look to understand what’s going on more. If you want a cleaner version of the code uncommented, here it is.

What about the Children part? Well, here I’m gonna introduce a new idea, but first let’s talk about what we’re gonna do: We will be checking if the current object has children (if #obj:GetChildren() > 0), if so, we create a Children table inside of its serialized table add, and we’ll be litearly re-doing what we already did with tableToSave, make a place for the classes, and initiate serialized objects and their properties into them. Isn’t that littearly the same thing InitProps does? Can’t we just set the Children table to InitProps(obj:GetChildren())? Yes, we can.

This my friend is called recursion. Recursion is making a function call itself over and over again until it hits a deadend, a base case where it has to stop (and yes, a function can call itself). In our case, let’s suppose an object has children, and its children has children. What would happen in the code is: initiate the object, then initiate its children, then initiate its children’s children. What if we didn’t know how many descendants there was? Using a while loop until there are no children isn’t that elegant. We can use recursion! If we implemented recursion what would happen is the function will keep on calling itself, initatiating the original objects, then initiating its children, it finds out that its children had children, and initiates its children, then it resumes the rest of initiating. I know, complicated indeed, sorry if you did not understand, so here is a good resource. Another example is with the CFrame serialization earlier, as we saw we had to serialize the vectors as well, we were calling Serialize inside of Serialize.

And we should be done!

local function InitProps(objects)
	local tableToSave = {}
	for _, obj in pairs(objects) do
		local class = obj.ClassName
		local t = tableToSave[class]
		if not(t) then
			tableToSave[class] = {}
			t = tableToSave[class]
		end
		local add = {}
		for _, Prop in pairs(Properties[obj.ClassName]) do
			add[Prop] = Serialize(obj[Prop])
		end
		local children = obj:GetChildren() --the children
		if #children > 0 then --if it has them
			add["Children"] = InitProps(children) --initiate the children
		end
		table.insert(t, add)
	end
	return tableToSave
end

If we were to game.HttpSerivce:JSONEncode() the returned table and print it.
We will get this:


Pretty cool right? You can see the serialized stuff.

And finally, we can wrap all this into one beautiful and basic function named Encode which is going to be the one we mainly use.

local function Encode(objects) --objects is the array of object I want serialized
	return InitProps(objects)
end

Just want to cover :JSONEncode() quickly, it converts a table into a JSON string (JSON stands for JavaScript Object Notation, in object in Javascript is basically what we call a dictionary in lua), JSON is universally used in programming to store (serialized) info, more info), along with other forms such as XML, in order to pass it around. The Datastore service actually converts what you save internally into JSON to be saved in their database. As I said, it’s used everywhere as a way of serialization. We serialize objects so the dictionary sample2 we give to Datastore can be serialized to JSON! If we don’t, the objects in the dictionary will actually just be replaced with null. And this is a mistake I did in the past, I used to manually :JSONEncode() my serialized objects dictionary sample2 before saving it, which is redundant, since it already gets automatically encoded internally.

After that, and simply, you can save the value returned from Encode with :SetAsync().

Now! Let’s move on to the deserialization! Or otherwise called, loading, perhaps unloading.

Again, we’ll start with the properties, as we talked earlier about serializating and deserializing proprites.

We’ll call this function, where prop is the name of the property (to not be confused with the previous Serialize function, where prop was the property’s value) and value is the property’s value. Again we talked about all this earlier.

local function Deserialize(prop, value)
	local r --this will be the returned deserialized property
	if prop == "Position" or prop == "Size" then
		r = Vector3.new(value.X, value.Y, value.Z)
	elseif prop == "CFrame" then
		r = CFrame.fromMatrix(Deserialize("Position", value.pos), Deserialize("Position", value.rX), Deserialize("Position", value.rY), Deserialize("Position", value.rZ))
	elseif prop == "BrickColor" then
		r = BrickColor.new(value)
	elseif prop == "Color" or prop == "Color3" then
		r = Color3.fromHSV(unpack(value))
	elseif prop == "Material" or prop == "Face" or prop == "Shape" then --you probably have to fill this one depending on the properties you're saving!
		r = Enum[value[1]][value[2]]
	else
		r = value --it gets here if the property 
	end
	return r --return it
end

Here we have to check for the property’s name, and not the value’s type, because with just the value given, I can’t know to what value I should serialize that; I could get a string, a string can be a serialized BrickColor value, or it can be just a string for a .Name property.

Now, to deserializing objects! We will call the function Create. It will take two parameters, where parent is the object you want to parent all objects to (children will still be parented to their original parent of course), and t is the serialized objects dictionary sample2 returned by Encode
The way this function will work is, by looping through the saved dictionary, and in each class table, loop through it, each time create an object of the current class, then loop through the serialized object, and setting the created object’s property prop, to the deserialized property’s value Deserialize(prop, value), pretty simple.
Again with recursion, if we find a Children table, we call Create on it, where the parent is the created object and it’s the list of serialized objects. If Children's objects had Children, it will take care of them as well, recursion does its natural thing.

local function Create(parent, t) 
	for class, _ in pairs(t) do --loop through classes, notice how I want class which is the key, I don't need the value so I do _
		for _, obj in pairs(t[class]) do --loop through class's serialized objects
			local object = Instance.new(class) --create the new object with the wanted class
			for prop, value in pairs(obj) do --loop through the serialized object's props and values
				if prop ~= "Children" then --we need to check if the current key inside of the serialized table is not the Children table
					object[prop] = Deserialize(prop, value) --do the magic
				else
					Create(object, value) --if it is the Children table, take care of it
				end
			end
			object.Parent = parent --parent it to the parent
		end
	end
end

And that’s it! Great!
We’re actually done, finally I’m just gonna make wrap all that in a neat function, just so we can keep up.

local function Decode(dic, slot)
	Create(slot, t)
end

We can now :GetAsync(), get the saved dictionary back sample2, and Decode it to spawn the objects back!
And we’re done! Hooray!

I took the liberty to turn it into a module and create a place to test it out, it’s fully commented (besides the serializer module) and you can download it!
serializer_place.rbxl (25.8 KB)
But just a disclaimer, like most of my articles, the purpose of this one is to learn and salvage info, not rip out the raw end product and use it in your works. Of course you can do that, but you need to know the appropriate changes to make, not ask people to make them for you.

You can spawn in some parts, save them, destroy them, then load them, and you’ll see that the same parts pop up. And if you leave the game, and re-join, then click load, the same parts will appear again!

local module = {}

local Properties = {
Part = {"Name", "Position", "Size", "Transparency", "BrickColor", "CanCollide", "CFrame", "Anchored", "Shape", "Material"},
Decal = {"Name", "Texture", "Transparency", "Face", "Color3"}
}

local function Serialize(prop)
	local type = typeof(prop)
	local r 
	if type == "BrickColor" then
		r = tostring(prop)
	elseif type == "CFrame" then
		r = {pos = Serialize(prop.Position), rX = Serialize(prop.rightVector), rY = Serialize(prop.upVector), rZ = Serialize(-prop.lookVector)}
	elseif type == "Vector3" then
		r = {X = prop.X, Y = prop.Y, Z = prop.Z}
	elseif type == "Color3" then
		r = {Color3.toHSV(prop)}
	elseif type == "EnumItem" then
		r = {string.split(tostring(prop), ".")[2], string.split(tostring(prop), ".")[3]}
	else
		r = prop
	end
	return r
end

local function Deserialize(prop, value)
	local r 
	if prop == "Position" or prop == "Size" then
		r = Vector3.new(value.X, value.Y, value.Z)
	elseif prop == "CFrame" then
		r = CFrame.fromMatrix(Deserialize("Position", value.pos), Deserialize("Position", value.rX), Deserialize("Position", value.rY), Deserialize("Position", value.rZ))
	elseif prop == "BrickColor" then
		r = BrickColor.new(value)
	elseif prop == "Color" or prop == "Color3" then
		r = Color3.fromHSV(unpack(value))
	elseif prop == "Material" or prop == "Face" or prop == "Shape" then
		r = Enum[value[1]][value[2]]
	else
		r = value
	end
	return r
end

local function InitProps(objects)
	local tableToSave = {}
	for _, obj in pairs(objects) do
		local class = obj.ClassName
		local t = tableToSave[class]
		if not(t) then
			tableToSave[class] = {}
			t = tableToSave[class]
		end
		local add = {}
		for _, prop in pairs(Properties[obj.ClassName]) do
			add[prop] = Serialize(obj[prop])
		end
		local children = obj:GetChildren()
		if #children > 0 then
			add["Children"] = InitProps(children)
		end
		table.insert(t, add)
	end
	return tableToSave
end

local function Create(parent, t)
	for class, _ in pairs(t) do
		for _, obj in pairs(t[class]) do
			local object = Instance.new(class)
			for prop, value in pairs(obj) do
				if prop ~= "Children" then
					object[prop] = Deserialize(prop, value)
				else
					Create(object, value)
				end
			end
			object.Parent = parent
		end
	end
end


function module.Encode(objects)
	return InitProps(objects)
end


function module.Decode(dic, slot)	
	Create(slot, dic)
end


return module

This is kind of important!
Note that, for games that have the same type of objects being used again and again (e.g. a building game, where you just have 3 (or more) types of building parts, like brick, dirt and plastic) you can simply save a table containing table where the key is each object’s name, and that table will hold a long list of serialized Vector3s that indicate where each block of that certain type is placed. And when unloading, loop through the table, and each time just :Clone() a pre-set object of the current type (assuming it’s stored in ServerStorage or ReplicatedStorage) and position it at the current position. This is better than having to save properties and extra info we don’t need. Of course this depends on the structure of your game, you might need to store additional info such as color maybe.

There some serializers that other people wrote and you have ability to use. Like this one by @Crazyman32.

Again, and as usual, have a wonderful day!

123 Likes

Thank you for the feedback! Can you give me a suggestion as what a basic example would be? I would like to make it easier for beginners!

4 Likes

Hey is it necessarily more efficient or the same to serialize in a string that can easily be deserialized with string patterns than your method? For example by creating spaces between numbers in a string, you can derive the Vector3 values or using string.sub to get Enum name after converting it to string.

1 Like

Well yeah, there are multiples ways to serialize stuff. Efficiency might imply that the amount of data (more correctly, the size of the saved json string) is lesser, speed can’t really be affected. I chose the dictionarry and table layout because it was the easiest to understand, and a better way to get introduced to serialization. When writing out a serializer for the public, you can be as absract as you want. In this case, more effficient serialization is better.

1 Like

Great tutorial that addresses common questions for newer developers. Nice work!

2 Likes

It is very easy to understand of course but what generally makes the serialization most effective as in what you said (less data)?

1 Like

You can actually test it out.

If you saved a Vector3 as a dictionarry, if you had a vector v3 = Vector3.new(2,3,8), so it’s something like this:

local save = game.HttpService:JSONEncode({X=v3.X, Y=v3.Y, Z=v3.Z})

print(save) --print how it looks
print(#save) --print the size of the saved string

image
If I saved it as a string where each component is seperated with a space. (Note that we don’t need to JSON encode a string).

local save = tostring(v3.X).." "..tostring(v3.Y).." "..tostring(v3.Z).." "

print(save)
print(#save)

image

7 vs 19
This difference will start making big changes in sizes of data saved when there a lot of objects.

4 Likes

Thank you for the response, that’s actually a pretty big difference that may be important in serialization.

1 Like

This was well written and explained, thank you for your contribution. I love serializing on a fundamental level. I just find it so cool how data is abstract to the point where you can deconstruct it into different data that can be used to reconstruct the original data.

Interesting thing to note:
You can’t do your method to MeshParts.

You can serialize and deserialize, but Roblox doesn’t allow you to set the mesh during runtime, so you wouldn’t be able to create the object back from the deserialized data.

7 Likes

Thank you for the overwhelming feedback!

Your note is very important actually, there are a lot of edge cases where you get stuck and can’t do anything about it.

2 Likes

What a nice tutorial, this will definitely help a lot of people.

Though I will mention, for CFrames, you don’t need to save position, right, up and lookvector (that’s 12 numbers),
if you just save the position and the rotation you can convert that back into a identical CFrame as well and it’s way more optimized.

In some cases you don’t even need to save all 3 numbers of the rotation, if you have a base builder game where players can only rotate objects sideways you only need the y axis.

Another way to optimalize is to do this:

local parts = {
 ["WoodenPlatform"] = {
   x = Position.x,
   y = Position.y,
   z = Position.z, roty = Rotation.y}
}

What I have done here is, I have a template part named “WoodenPlatform” inside ReplicatedStorage, it already has a size of 5, 1, 1 and the brick color and material is already set, etc and players can only rotate objects sideways, this takes up way less memory and allows bigger creations to be saved!

Now all we need to do is use pairs() since the key of the values in the table has the name of the part and pairs returns the key’s name when you print it.
So if we do…

for key, v in pairs(parts) do
   print(key, v.x, v.y, v.z, v.roty)
end

key will print the part’s name, and the rest explains itself, we use the key’s name
to refer to the WoodenPlatform that we already have existing in ReplicatedStorage.

I would use this to save lots of memory in games where you can build big things.
A way to also optimalize is
if you don’t want part shifting or parts to snap to a certain grid (wether it be 1 stud or 0.1 stud) you can do

math.floor((Position.X * 10) + 0.5)

and when you deserialize it you simply divide the value by 10 or 100.
When you multiply the number by 10 and round it up you take away the comma (.) and the number imprecision (0.09054303), this saves even more memory!

Hope that helps, also correct me if I’m wrong somewhere, this is from personal experience.

4 Likes

Of course, there are ways to optimize based on your specific use case. His tutorial was written with a more general audience in mind.

5 Likes

Thank you so much for this! I am quite new at scripting, so this is extremely useful for me! I can’t wait for new tutorials by you, keep it up!

2 Likes

This is a nice tutorial, honestly. It shows one of the huge possibilities of programming, and in programming, anything is possible.

Seriously though, this is a really great explanation. I’ve been recently wondering how to save parts and all. Before seeing this tutorial, I felt like it would be an advanced-level process and set of huge complications, but now that I’ve seen this tutorial, it’s as simple as storing properties in tables, encoding + decoding it, and using the serialized properties to create a new Instance with all of the properties.

Good job on the explanation! :+1:

2 Likes

Pretty sure SpecialMeshes can be changed during runtime, so if you would REALLY need to keep the same mesh + texture when serializing / deserializing, you could probably use those. But unfortunately I don’t think materials can be applied on a part with SpecialMeshes. BrickColor property also won’t work on SpecialMeshes, as that Instance has a VertexColor property, so BrickColor cannot override it.

2 Likes

More importantly, SpecialMeshes do not affect collision geometry so even if you could bring it back visually, it wouldn’t actually be functionally the same as the original serialized mesh.

5 Likes

What was the point of using the HTTP service? Doesn’t roblox already convert tables to JSON, and what makes saving strings better than tables? I’ve done this multiple times before without using the HTTP service and have not run into any issues.

1 Like

Did you read the OP? The tutorial explains serialization. In this case an instance is serialized into a table since instances can’t be saved to data stores. A table can indeed be saved in a data store. Roblox indeed JSON-encodes the values to save. Which is also why you shouldn’t do this yourself as you could take up to over 2x the space.

I don’t understand the point you are trying to make, Did he use the Encode function on the instance itself?

The HTTPService contains the JSONEncode() function, for viewing data when it’s in serialized format. It’s not necessary for actually serializing the data but for debugging it’s incredibly useful for seeing how the computer has laid out your table. print(table) will only print the memory pointer, so you need to encode it as text to view it’s layout and contents.

The reason the encoding function is inside the HTTP service is because usually you need it when interacting with web API’s. In this case, we’re only using it for debugging, not actually sending data across the net.