Skip to content

[Bug]: encodeValue crashes with "Cannot read properties of undefined" on MapSchema with nested Schema types #223

@miketests

Description

@miketests

Bug description

Description

When encoding a MapSchema containing nested Schema instances, encodeValue in EncodeOperation.ts:37 receives undefined as the type parameter, causing a crash on type[Symbol.metadata].

The @type() decorators run correctly — Symbol.metadata is populated on all classes (verified via logging). The crash happens at encode time, not at decoration time.

Environment

  • @colyseus/schema: 3.0.76
  • colyseus: 0.16.5
  • Node.js: 24.13.0
  • TypeScript: 5.9.3
  • Runtime: tsx 4.21.0 with --no-experimental-strip-types (forces esbuild, which correctly handles experimentalDecorators)
  • tsconfig: "experimentalDecorators": true

Error

TypeError: Cannot read properties of undefined (reading 'Symbol(Symbol.metadata)')
    at encodeValue (EncodeOperation.ts:37:20)
    at encodeKeyValueOperation (EncodeOperation.ts:150:5)
    at Encoder.encode (Encoder.ts:119:17)
    at Encoder.encodeAll (Encoder.ts:155:21)

The crash is at this line in EncodeOperation.ts:37:

} else if (type[Symbol.metadata] !== undefined) {

where type is undefined.

Notes

  • Encoding works fine with no entries in the MapSchema — the crash only happens when encoding a map entry (ADD operation).
  • All class metadata is correctly populated (decorators run successfully).
  • @colyseus/schema@2.0.37 does not have this issue — MapSchema encoding works correctly there.
  • This may be related to how Encoder resolves the child type of a MapSchema before passing it to encodeValue.

Optional: Minimal reproduction

Minimal Reproduction

import { Schema, type, MapSchema, Encoder } from '@colyseus/schema'

class Player extends Schema {
  @type('string') sessionId: string = ''
  @type('float32') x: number = 0
}

class GameState extends Schema {
  @type({ map: Player }) players = new MapSchema<Player>()
  @type('string') mapId: string = ''
}

// Verify metadata is populated correctly
console.log('Player metadata:', (Player as any)[Symbol.metadata])
// → { '0': { type: 'string', index: 0, name: 'sessionId' }, '1': { type: 'float32', index: 1, name: 'x' } }

console.log('GameState metadata:', (GameState as any)[Symbol.metadata])
// → { '0': { type: { map: [class Player] }, index: 0, name: 'players' }, '1': { type: 'string', index: 1, name: 'mapId' } }

const state = new GameState()
const encoder = new Encoder(state)

// ✅ This works — empty state
encoder.encodeAll()

// Add a player to the map
const player = new Player()
player.sessionId = 'abc'
player.x = 100
state.players.set('abc', player)

// ❌ This crashes
encoder.encode()

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions