KistePix

Home › The Aseprite JSON format

The Aseprite JSON format, explained

When a pixel editor exports a sprite sheet, it drops a JSON file next to the PNG describing where each frame sits and how long it should be shown. The schema comes from Aseprite and has become the de facto standard. Here is what every field means — and how to actually use the file in an engine.

What the file is for

Without it you would have to tell your engine by hand: frames are 32 pixels wide, there are eight of them, they sit in one row, each lasts 100 milliseconds. Change the size later or add a frame, and you get to update that everywhere. The JSON file carries those facts with the image — the sheet describes itself.

A complete example

This is what KistePix writes next to a four-frame sheet of 32 × 32 pixels (trimmed to two frames here):

{
  "frames": [
    {
      "filename": "hero 0.png",
      "frame":            { "x": 0,  "y": 0, "w": 32, "h": 32 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0,  "y": 0, "w": 32, "h": 32 },
      "sourceSize":       { "w": 32, "h": 32 },
      "duration": 100
    },
    {
      "filename": "hero 1.png",
      "frame":            { "x": 32, "y": 0, "w": 32, "h": 32 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0,  "y": 0, "w": 32, "h": 32 },
      "sourceSize":       { "w": 32, "h": 32 },
      "duration": 150
    }
  ],
  "meta": {
    "app": "kiste-lang.org/kistepix",
    "version": "1.0",
    "image": "hero.png",
    "format": "RGBA8888",
    "size": { "w": 128, "h": 32 },
    "scale": "1",
    "loop": true
  }
}

The fields of a frame

FieldMeaning
filenameThe name of this frame. Historically the filename it would have had as a single image. Many engines use it as the key to address a frame.
frameThe rectangle in the sheet: x/y is the top-left corner, w/h the size. This is the field an engine truly needs — this is where it cuts.
rotatedWhether the frame is stored rotated by 90°. Some packers rotate images to save space. KistePix never does, so this is always false.
trimmedWhether empty margins were cropped away. Saves space, complicates the maths. KistePix does not trim, so false.
spriteSourceSizeWhere the (possibly trimmed) cut-out sat within the original image. With no trimming it is the full rectangle from 0/0 — which is exactly what KistePix writes.
sourceSizeThe size of the original, untrimmed image. It is what lets a trimmed frame still land in the right place.
durationHow long this frame is shown, in milliseconds. Each frame has its own — which is why an animation can be unevenly timed.

The one thing to get right: frame describes the position in the sheet; sourceSize and spriteSourceSize describe the position in the original image. Mix them up and your sprites come out offset. As long as trimmed is false they agree, and you can just use frame.

The meta block

What KistePix does not write, so you do not build on it: Aseprite's meta block can also carry frameTags (named ranges such as "walk" or "idle"), layers and slices. KistePix exports an animation as a plain row of frames, with no named ranges. So tools that insist on frameTags — Phaser's createFromAseprite, for one — will find nothing to work with. The ordinary texture-atlas route works fine instead.

Array or hash?

Aseprite can store the frames two ways, and importers routinely trip over it. In the array form, frames is a list and the name lives inside each entry as filename:

"frames": [ { "filename": "hero 0.png", "frame": { … } }, … ]

In the hash form, frames is an object keyed by those names:

"frames": { "hero 0.png": { "frame": { … } }, … }

The content is identical; the importer simply has to know which one it is getting — Phaser, for instance, distinguishes "JSON Array" from "JSON Hash" when loading. KistePix always writes the array form, so frame order is already fixed by the list.

Loading it in an engine

Phaser

The array form is exactly what Phaser expects from a texture atlas:

this.load.atlas('hero', 'hero.png', 'hero.json');

// play the frames in order:
this.anims.create({
  key: 'walk',
  frames: [
    { key: 'hero', frame: 'hero 0.png' },
    { key: 'hero', frame: 'hero 1.png' }
  ],
  frameRate: 10,
  repeat: -1
});

Godot

Godot has no built-in reader for this format; community add-ons exist. You may not need one: because all frames are the same size and sit in a single row, you can hand the PNG to an AnimatedSprite2D and use SpriteFrames › Add Frames from Sheet, where you only state the number of columns. Set the durations from the JSON in the animation player. And set the PNG's import filter to Nearest, or the pixel art comes out blurred.

Unity

Unity does not read the format natively either. Since the sheet is a regular grid, the built-in route is enough: texture type Sprite (2D and UI), sprite mode Multiple, then in the Sprite Editor Slice › Grid By Cell Size with your frame size. Set Filter Mode to Point (no filter) and Compression to None, or compression will smear individual pixels.

GameMaker

GameMaker is the odd one out here: it has no importer for Aseprite JSON, and there is no way to hand it the file on the side. That turns out not to matter, because GameMaker does not need it — it reads the one piece of information it is missing from the file name.

If the name ends in _stripN, where N is the number of frames, GameMaker slices the image on import by itself. So ball.png with five frames becomes ball_strip5.png. Drag that file into the IDE and it is cut into five frames, after which GameMaker strips the suffix back out of the sprite name. If you prefer doing it by hand: Sprite editor › Import strip image, then enter the frame count or the frame width and height. Those values are in the JSON under frames[0].frame.w and .h; margin and spacing stay at 0, because the cells sit flush against each other.

KistePix does this for you: pick the Sprite-Sheet (GameMaker) format on export, type ball — and you get ball_strip5.png. The program works out the frame count itself.

One difference from the other engines remains: GameMaker knows only one speed for the whole sprite, not a duration per frame. The duration values from the JSON cannot be mapped individually there — you set a matching sprite speed instead.

Reading it yourself

If you parse the file directly, it is a handful of lines — here in JavaScript:

const sheet  = JSON.parse(text);
const frames = sheet.frames.map(f => ({
  x: f.frame.x, y: f.frame.y,
  w: f.frame.w, h: f.frame.h,
  ms: f.duration
}));
const total = frames.reduce((s, f) => s + f.ms, 0);  // length of the loop

Sheets that describe themselves

KistePix writes the PNG and the JSON in one go — carrying each frame's duration exactly as you set it on the timeline.

Get KistePix Guide: how to make a sprite sheet