{"record":{"id":"4ec557352d7f93ab","repo":"ppy/osu","slug":"expected-type-token","errorCode":null,"errorMessage":"Expected $type token.","messagePattern":"Expected \\$type token\\.","errorType":"exception","errorClass":"JsonException","httpStatus":null,"severity":"error","filePath":"osu.Game/IO/Serialization/Converters/TypedListConverter.cs","lineNumber":63,"sourceCode":"\r\n            var obj = JObject.Load(reader);\r\n\r\n            if (obj[\"$lookup_table\"] == null)\r\n                return list;\r\n\r\n            var lookupTable = serializer.Deserialize<List<string>>(obj[\"$lookup_table\"].CreateReader());\r\n            if (lookupTable == null)\r\n                return list;\r\n\r\n            if (obj[\"$items\"] == null)\r\n                return list;\r\n\r\n            foreach (var tok in obj[\"$items\"])\r\n            {\r\n                var itemReader = tok.CreateReader();\r\n\r\n                if (tok[\"$type\"] == null)\r\n                    throw new JsonException(\"Expected $type token.\");\r\n\r\n                // Prevent instantiation of types that do not inherit the type targetted by this converter\r\n                Type type = Type.GetType(lookupTable[(int)tok[\"$type\"]]).AsNonNull();\r\n                if (!type.IsAssignableTo(typeof(T)))\r\n                    continue;\r\n\r\n                var instance = (T)Activator.CreateInstance(type)!;\r\n                serializer.Populate(itemReader, instance);\r\n\r\n                list.Add(instance);\r\n            }\r\n\r\n            return list;\r\n        }\r\n\r\n        public override void WriteJson(JsonWriter writer, IReadOnlyList<T> value, JsonSerializer serializer)\r\n        {\r\n            var lookupTable = new List<string>();\r","sourceCodeStart":45,"sourceCodeEnd":81,"githubUrl":"https://github.com/ppy/osu/blob/d9c73e12adff2feaae4a3e158d36fe5883faf6ca/osu.Game/IO/Serialization/Converters/TypedListConverter.cs#L45-L81","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nif (tok[\"$type\"] == null)\n    throw new JsonException(\"Expected $type token.\");\n\n// after (tolerant: skip malformed entries)\nif (tok[\"$type\"] == null)\n{\n    Logger.Log($\"Skipped typed-list item missing $type token.\", LoggingTarget.Database, LogLevel.Important);\n    continue;\n}","handlingStrategy":"validation","validationCode":"// Validate each $items entry has an int $type within lookup bounds before deserialising.\nforeach (var tok in payload[\"$items\"])\n    if (tok[\"$type\"] == null || (int)tok[\"$type\"] < 0 || (int)tok[\"$type\"] >= lookupTable.Count)\n        throw new InvalidDataException(\"Malformed typed-list item: missing/invalid $type\");","typeGuard":"static bool HasValidTypeToken(JToken item, List<string> lookup)\n    => item[\"$type\"] != null && (int)item[\"$type\"] >= 0 && (int)item[\"$type\"] < lookup.Count;","tryCatchPattern":"try { var list = serializer.Deserialize<IReadOnlyList<T>>(reader); }\ncatch (JsonException ex) when (ex.Message.Contains(\"$type token\"))\n{ /* reject/re-serialise the payload from a trusted source */ }","preventionTips":["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."],"tags":["serialization","json","beatmap","schema","migration"],"backgroundTag":null,"analyzedSha":"d9c73e12adff2feaae4a3e158d36fe5883faf6ca","analyzedAt":"2026-08-13T14:12:54.015Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}