Skip to content

Decode Unity JSON-RPC error data into typed structs instead of map[string]any #2042

Description

@hatayama

Problem

The Unity JSON-RPC error data payload is typed on the C# side and untyped on the Go side, so the two halves of the same contract cannot be checked against each other.

C# declares a DTO per error type:

Packages/src/Editor/Infrastructure/Api/ServerBusyErrorData.cs

public class ServerBusyErrorData : JsonRpcErrorData
{
    public override string type => JsonRpcErrorTypes.ServerBusy;
    public string runningToolName { get; }
    public bool? isCompiling { get; }
    public bool? isUpdating { get; }
    public double? secondsSinceLastMainThreadTick { get; }
    ...
}

Go decodes the same payload into map[string]any and reads it back by string key:

cli/common/errors/busy_editor_state.go

func unityServerBusyNextActions(data map[string]any) []string {
	if rpcBoolData(data, "isCompiling") { ... }
	if rpcStringData(data, "runningToolName") == "compile" { ... }
	if stallSeconds, ok := rpcFloatData(data, "secondsSinceLastMainThreadTick"); ok { ... }

Every key is a bare string literal, and the accessors (rpcBoolData, rpcFloatData, copyOptionalBoolField) silently fall back to the zero value on a miss.

Consequences:

  • Renaming a field in ServerBusyErrorData compiles cleanly on both sides and silently disables the Go branch that depended on it. runningToolName gates the compile-reattach guidance in NextActions, so the failure mode is a lost recovery hint rather than an error.
  • A key typo in Go is equally invisible: rpcBoolData returns false, which is a legal value.
  • No static analysis can relate the C# field to the Go reader, because the only thing connecting them is a string literal.

Two more untyped reads exist on the same boundary:

  • cli/common/errors/error_envelope_classification.go:191 — data["type"].(string)
  • cli/common/errors/error_envelope.go:178 — data["updateCommand"].(string)

Suggested direction

Decode into Go structs with json tags mirroring each JsonRpcErrorData subclass, and read fields instead of map keys. json.Unmarshal into a typed struct keeps the key names in exactly one place per side, and pointer fields preserve the NullValueHandling.Ignore optionality that the current bool? fields carry.

Worth deciding as part of the work: whether a round-trip test that feeds real Unity payloads into the Go decoder is enough to catch renames, or whether the field names should be generated from a single source shared with cli/common/clicontract.

Scope notes

  • Behaviour-preserving refactor. The emitted error envelope should be byte-identical for every existing payload, including the omit-when-absent behaviour of unityServerBusyEditorActivitySummary.
  • Unrelated to Details and NextActions on the outgoing side: those are write-only in Go product code (all assignments, all within the project runner) and are consumed by agents reading the JSON, not by another component.
  • Found while reviewing feat: configurable compile wait timeout and working timeout recovery #2036, which added the runningToolName == "compile" branch above. Not a defect in that change.

Activity

  1. hatayama commented on Aug 17, 2026

    @hatayama
    OwnerAuthor

    Resolved by #2214 (merged into v3-beta as c915d9a). Unity JSON-RPC error data is now decoded through typed Go structs mirroring the C# DTOs, with shared contract fixtures (tests/contracts/server_busy_error_contract.json and cli_update_required_error_contract.json) verified field-by-field on both sides.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions