Skip to content

Null notes/chords in a Chord or Phrase are accepted, then crash later with NullReferenceException in ToString() and SaveAsync #187

Open

Description

@matt-edmondson

What's wrong

Chord and Phrase check their collections for null but never check the elements inside them:

  • Chord(IEnumerable<Note>) is in Keybinding/Models/MusicalTypes.cs around lines 150-159.
  • Phrase(IEnumerable<Chord>) is around lines 359-368.
  • Phrase(Chord) (around line 374) calls this([chord]) with no Ensure.NotNull, unlike Chord(Note).
  • The [JsonConstructor] overloads of Chord, Phrase and Profile (Models/Profile.cs:41-50) all go through these constructors.

A bad value is therefore accepted when it is built and only fails later, somewhere unrelated.

Reproduction

Each case below was run against the current main:

  • JsonSerializer.Deserialize<Chord>("{\"Notes\":[null]}") succeeds, giving Notes.Count == 1 and Notes[0] == null. Calling ToString() on the result then throws NullReferenceException in IsModifierNote.
  • {"Notes":[null,{"Key":"A"}]} throws NullReferenceException from the OrderBy key selector instead of a JsonException.
  • {"Notes":null} throws ArgumentNullException and {"Notes":[]} throws ArgumentException. Callers that catch JsonException miss both.
  • new Phrase((Chord)null!) and Deserialize<Phrase>("{\"Sequence\":[null]}") both succeed, and ToString() then throws NullReferenceException.
  • A profile loaded from {"Id":"a","Name":"A","Chords":{"x.y":{"Notes":[null]}}} reports ChordCount == 1. Once it is registered, KeybindingManager.SaveAsync → JsonKeybindingRepository.SaveProfileAsync throws NullReferenceException at n.ToString() (Services/JsonKeybindingRepository.cs:66), and the save fails part-way through.

NoteJsonConverter already wraps invalid keys in JsonException (Models/NoteJsonConverter.cs:318-328). That is the contract the other types should follow.

Suggested fix

  • In the Chord and Phrase constructors, reject null elements with ArgumentException before Distinct/OrderBy.
  • Add Ensure.NotNull(chord) to Phrase(Chord).
  • In the private [JsonConstructor] overloads, catch ArgumentException and rethrow it as JsonException (inner exception kept), the same way NoteJsonConverter does.

Acceptance criteria

  • Deserializing Chord or Phrase JSON with null, missing or empty Notes/Sequence throws JsonException.
  • Constructing either type with a null element throws ArgumentException.
  • Regression tests cover each case above.

Activity

  1. matt-edmondson commented on Oct 18, 2026

    @matt-edmondson
    ContributorAuthor

    Triage


    Generated by Claude Code

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

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions