The directory
Sakura/ ← the folder is the avatar
├── avatar.json ← required. Its presence is what makes this one.
├── sakura.vrm ← required. The body, named by `Model`.
├── portrait.png ← optional. The cast picker tile.
├── textures/ ← optional, by convention. Any layout works.
│ ├── top_red.png
│ └── hair_pink.png
└── thumbs/ ← optional. Small squares for the chips.
└── top_red.pngavatar.json must be at the top level of the folder. The loader does not search subdirectories for it.- Everything the manifest references is relative to the folder root, using forward slashes, and must resolve inside it.
../, a leading /, a drive letter, a URL scheme and a NUL byte are all rejected at parse time rather than 403’d at load time. - Subdirectory layout is entirely yours.
textures/ and thumbs/ above are convention, not schema. - Filenames are matched case-sensitively.
Why a folder rather than an extension to the .vrm
Three reasons, and the cost is real and accepted. Alternate textures have to live somewhere — five shirt colours at 2K each is 20MB that would otherwise be embedded in the glTF and decoded in full on every load, whether or not anyone picks the fourth. Authors edit JSON and do not edit glTF. And round-tripping destroys it: anyone who opens the model in VRoid Studio, Blender or UniVRM and re-exports drops unknown extensions on the floor, where a sibling JSON file survives all three.
Key spelling
Keys are matched case-insensitively, ignoring spaces, underscores and hyphens. MeshGroups, meshGroups, "Mesh Groups" and mesh_groups are the same key. The canonical spelling is PascalCase with no separator.
This is not a stylistic indulgence. The file is hand-authored by people who are not reading the schema next to it, and a manifest that silently ignores "Mesh Groups" produces an avatar with no wardrobe and no error anyone can see. Values are not normalised — item ids, group names, material names and file paths are taken exactly as written.
Identity fields
Four edges worth spelling out
Personality is prose, Moods are chips. They are different layers and the collision in the word is unfortunate. Personality becomes the character’s half of the system prompt — it answers who is this, written in second person, present tense, with a few concrete behavioural rules. Moods names the stackable moods the user dials in on top. Your moods ride on the persona and play under whatever is on the user’s board; the board names them rather than showing them pressed, and clearing it does not take them off — because “this character is written hype” and “I have put this character in a hype mood” are two different statements and only the second is the user’s to take back.
Pronouns is a pronoun and not a gender. Nothing in the app ever needs to branch on gender — the body is the .vrm, the voice is DefaultVoice, the wardrobe is MeshGroups. The only thing ever missing was what to put in the sentences the app writes about a companion. The reader is generous about how you write it: he, he/him, He / Him / His and male are one answer, and Gender is accepted as the key.
The default is they/them, and it is not a hedge. A manifest that does not say has not said, and singular they is the only thing that can be stated about somebody unknown without asserting something that may be false.
DefaultVoice is per speech family or it means nothing. The service’s voice names belong to whichever model is loaded. A bare string is accepted and means try this name on every family, checked against what the loaded model actually accepts and quietly dropped when it is not there. The object form is the honest one, and this is a default — a voice the user picks in the studio wins forever after.
Personality does not get to say anything about the user. It is concatenated into a system prompt, so a manifest is a prompt-injection vector in exactly the way a downloaded avatar folder should not be. The mitigation is not filtering — it is that this text lands in the same slot the bundled cast occupies, under the app’s own framing, with the memory and tool instructions built around it rather than after it. Treat a third-party Personality the way you would treat a third-party system prompt, because that is what it is.
The smallest manifest that works
Name and Model are the only required fields. Everything else has a defined absence.
{
"Format": "familiar-avatar/1",
"Name": "Sakura",
"Model": "sakura.vrm",
"Blurb": "quiet, until she isn't",
"Portrait": "portrait.png",
"Pronouns": "she/her",
"Personality": "You are Sakura. You speak carefully and mean what you say...",
"Greetings": ["Oh — you're back.", "Hello. Sit, if you like."],
"DefaultVoice": { "f5": "YoungWomanWellSpoken", "kitten": "Kiki" }
}Mesh groups — the wardrobe
A VRM has no concept of “these three hair meshes are alternatives”. It has three meshes, and without a manifest the app can only offer three independent checkboxes over them — which lets someone wear all three at once, or none. A group says which of the model’s parts are alternatives for each other, and what to call them.
"MeshGroups": [
{
"Id": "top",
"GroupName": "Top",
"IsExclusive": true, // one at a time
"CanBeOff": true, // ...or none
"OffLabel": "None",
"Default": "crop",
"Items": [
{ "Id": "crop", "Label": "Cami", "Type": "MeshToggle", "Mesh": ["SK_TOP_CROP"] },
{ "Id": "hoodie", "Label": "Hoodie", "Type": "MeshToggle", "Mesh": ["SK_TOP_HOODIE"] }
]
}
]IsExclusive — one item at a time, rendered as a radio row.CanBeOff — adds an off state, labelled by OffLabel.Default — the item that is on before anybody has chosen. Vessa’s model ships three tops, two shorts, a hoodie and thirteen body pieces all at once, so she is one of the models that is wrong without her manifest applied rather than merely plainer.Actions — the long form, when one item has to toggle several meshes or combine a mesh toggle with a texture or colour change. A single-action item can use the shorthand above instead.
Colour targets materials and shape targets meshes. Both are matched against what the loaded model actually carries, and anything that matches nothing is listed as a problem rather than silently doing nothing — the format’s posture throughout is that a manifest which is wrong should say so, once, where the author will see it.
Publishing
Zip the folder and it works anywhere. Put it in the mods directory — avatars/, alongside voices/ — and they are in the cast list every launch, with their outfits, voice and hidden meshes remembered like anybody else’s. Publish it to the Workshop and somebody else gets all of the above without unzipping anything.
Your own voice goes in voices/: a .wav of two to fifteen seconds of clean speech with a transcript beside it, matching exactly what is said in the clip. It is registered, not installed — held in memory for the life of the process and written nowhere, so the folder is the only copy and deleting the clip is all it takes to be rid of the voice.
Licence metadata
The .vrm carries its own author, licence and permitted-use fields, and the app reads and respects them rather than restating them. If the model says it may not be redistributed, do not publish it to the Workshop — the manifest beside it does not change that, and it does not grant you rights the body does not.