Skip to content

Store Roblox values safely

A DataStore holds JSON and nothing else. Write a Vector3 into a player’s profile and the save fails, but it fails late and it fails quietly: the value travels through every step by reference, without complaint, until the write itself.

Serialize turns those values into something storage accepts, and finds the shapes that would be lost on the way back.

Encode on the way in, decode on the way out

Section titled “Encode on the way in, decode on the way out”
-- Saving a spawn point the player chose.
data.Home = Twill.Serialize.Encode(spawnPart.Position)
-- Reading it back, wherever that happens.
local home = Twill.Serialize.Decode(data.Home)
part.Position = home

Both walk the whole tree, so a table of mixed values needs one call rather than one per field:

data.Loadout = Twill.Serialize.Encode({
Colour = weapon.Colour, -- Color3
Offset = weapon.GripOffset, -- CFrame
Slot = 3, -- left exactly as it is
})

Anything unrecognised passes through untouched, so running either over ordinary data costs a walk and changes nothing. That makes Decode safe to run over a profile that holds a mix of encoded and plain values, without knowing which is which.

Twill will not encode for you. Data.Edit refuses an unstorable value and says so, rather than rewriting what you handed it.

local outcome = Twill.Data.Edit(userId, "main", "Home", somePosition)
--> "unsupported"

The reason is that guessing on the way in means guessing on the way back out. A save that quietly rewrites your value would hand you back something else later, and you would find out long after the write.

Encode when a value enters saved data and decode when it leaves. Keeping profiles encoded in memory means every reader has to remember to decode, and one that forgets reads a table where it expected a Vector3.

Vector3, Vector2, CFrame, Color3, UDim, UDim2, BrickColor, NumberRange, and EnumItem.

Anything else is not a storage problem to solve here. An Instance cannot be saved at all; save something that identifies it, such as a name or an id, and find it again on load.

Failing to save is loud. Saving something that comes back wrong is not, and those are the shapes worth knowing.

local where, what = Twill.Serialize.FindUnstorable(data)
if where then
logger:Warn(`{where} holds {what}`)
end
ShapeWhat happens without the check
Both named and numbered keys in one tableNumbers return as the strings "1", "2", and every reader sees nil.
Gaps in a numberingThe gap and everything after it is dropped.
A key that is not a string or a whole numberThe entry vanishes.
NaN or infinityThe write fails.
Text that is not valid UTF-8The write fails.

The first two are the dangerous ones, because nothing reports them at any point.

-- Looks harmless. Comes back unreadable.
data.Slots = {
[1] = "sword",
[2] = "shield",
Favourite = "sword",
}

Split it instead:

data.Slots = { "sword", "shield" }
data.Favourite = "sword"

FindUnstorable is cheap enough to run in Studio against a profile you have been playing with, which is the fastest way to catch a shape that only appears after a few sessions.

local RunService = game:GetService("RunService")
local Twill = require("@game/ServerScriptService/Twill")
local logger = Twill.Log.new("DataAudit")
function DataAuditService.Start()
if not RunService:IsStudio() then
return
end
Twill.Watch.Players(function(player)
task.delay(10, function()
local data = Twill.Data.Get(player)
if not data then
return
end
local where, what = Twill.Serialize.FindUnstorable(data)
if where then
logger:Warn(`{player.Name}: {where} holds {what}`)
end
end)
end)
end

Two modules answer different questions, and reaching for the wrong one is common.

UseWhen
SerializeA person or another system reads the field, or the value must stay exact.
CompressOnly size matters, and the field is opaque anyway.

Compress is lossy by design and produces something unreadable. Serialize keeps the value exact and keeps the profile legible in the DataStore console, which matters the day you have to look at one by hand. See Send large payloads for the other half.