ppy/osu · error · JsonException
Expected $type token.
Error message
Expected $type token.
What it means
Thrown by TypedListConverter<T>.ReadJson while iterating $items in a typed-list JSON payload: each item object must carry a $type token indexing into $lookup_table. If an item lacks $type, the converter cannot resolve which concrete type to instantiate, so it throws a JsonException before the lookup index read.
Source
Thrown at osu.Game/IO/Serialization/Converters/TypedListConverter.cs:63
var obj = JObject.Load(reader);
if (obj["$lookup_table"] == null)
return list;
var lookupTable = serializer.Deserialize<List<string>>(obj["$lookup_table"].CreateReader());
if (lookupTable == null)
return list;
if (obj["$items"] == null)
return list;
foreach (var tok in obj["$items"])
{
var itemReader = tok.CreateReader();
if (tok["$type"] == null)
throw new JsonException("Expected $type token.");
// Prevent instantiation of types that do not inherit the type targetted by this converter
Type type = Type.GetType(lookupTable[(int)tok["$type"]]).AsNonNull();
if (!type.IsAssignableTo(typeof(T)))
continue;
var instance = (T)Activator.CreateInstance(type)!;
serializer.Populate(itemReader, instance);
list.Add(instance);
}
return list;
}
public override void WriteJson(JsonWriter writer, IReadOnlyList<T> value, JsonSerializer serializer)
{
var lookupTable = new List<string>();
View on GitHub (pinned to d9c73e12ad)
Solutions
- Ensure the JSON was produced by the matching WriteJson (it always emits $type first via JProperty); re-serialise the source if needed.
- Validate the payload schema before deserialisation: every entry in $items must contain $type as an int within $lookup_table bounds.
- If tolerating old/partial data is required, subclass/override ReadJson to skip items missing $type with a warning instead of throwing.
Example fix
// before
if (tok["$type"] == null)
throw new JsonException("Expected $type token.");
// after (tolerant: skip malformed entries)
if (tok["$type"] == null)
{
Logger.Log($"Skipped typed-list item missing $type token.", LoggingTarget.Database, LogLevel.Important);
continue;
} Defensive patterns
Strategy: validation
Validate before calling
// Validate each $items entry has an int $type within lookup bounds before deserialising.
foreach (var tok in payload["$items"])
if (tok["$type"] == null || (int)tok["$type"] < 0 || (int)tok["$type"] >= lookupTable.Count)
throw new InvalidDataException("Malformed typed-list item: missing/invalid $type"); Type guard
static bool HasValidTypeToken(JToken item, List<string> lookup)
=> item["$type"] != null && (int)item["$type"] >= 0 && (int)item["$type"] < lookup.Count; Try / catch
try { var list = serializer.Deserialize<IReadOnlyList<T>>(reader); }
catch (JsonException ex) when (ex.Message.Contains("$type token"))
{ /* reject/re-serialise the payload from a trusted source */ } Prevention
- Always round-trip typed-list JSON through the converter's own WriteJson to guarantee $type emission.
- Treat externally-edited beatmap JSON as untrusted; validate the $items/$lookup_table schema first.
- If backward compatibility is needed, consider an overriding converter that skips malformed items.
When it happens
Trigger: Deserialising a beatmap JSON (or any TypedListConverter-backed list) whose $items entries are missing the $type property — caused by hand-edited JSON, a serialiser that omitted $type, or a version/format mismatch between writer and reader.
Common situations: Loading a .osu beatmap JSON edited externally; a serialisation round-trip through a custom JsonSerialiserSettings that strips null/zero tokens; cross-version beatmap files whose schema predates $type.
Related errors
- Beatmap file {b.File!.Filename} could not be opened!
- Timed out while loading beatmap ({beatmapInfo}).
- Creating ruleset instance failed when attempting to create p
- Beatmap can not be converted for the ruleset (ruleset: {rule
- Beatmap contains no hit objects!
AI-assisted analysis of ppy/osu@d9c73e12ad (2026-08-13).
Data as JSON: /api/errors/4ec557352d7f93ab.
Report an issue: GitHub.