Skip to content

[CanBeNull] on a collection field also makes its elements nullable (per-element flag on disk) #57

Description

@kamronbatman

Summary

[CanBeNull] on a collection field is also applied to the collection's elements (and dictionary keys/values), because every collection rule passes the field's full attribute list down when it resolves the element rule.

Where

ListMigrationRule, HashSetMigrationRule, SortedSetMigrationRule, ArrayMigrationRule, DictionaryMigrationRule all call SerializableMigrationRulesEngine.GenerateSerializableProperty(compilation, "…Entry", elementType, 0, attributes, parentSymbol, null) with the collection field's attributes. Element rules that honour [CanBeNull] (RawSerializableMigrationRule, PrimitiveUOTypeMigrationRule, nested collection rules) then emit per-element null handling.

Observed

[CanBeNull]
[SerializableField(2)]
private List<SpawnerEntry> _entryList;   // SpawnerEntry is [SerializationGenerator]

generates (Serialize):

if (_entryList != default)
{
    writer.Write(true);
    var _entryListCount = _entryList?.Count ?? 0;
    writer.WriteEncodedInt(_entryListCount);
    if (_entryListCount > 0)
    {
        foreach (var _entryListEntry in _entryList!)
        {
            if (_entryListEntry != default) { writer.Write(true); _entryListEntry.Serialize(writer); }
            else { writer.Write(false); }
        }
    }
}

and the matching read path allocates _entryListEntry = default into the list when the per-element flag is false. The schema records the flag twice: ["@CanBeNull", "Server.Engines.Spawners.SpawnerEntry", "RawSerializableMigrationRule", "DeserializationRequiresParent", "@CanBeNull"].

Why it matters

  • One extra byte per element on disk for every [CanBeNull] collection whose element rule honours the flag.
  • The read path admits null elements into a list the author never declared as holding nulls.
  • There is no way to say "the collection may be null, its elements may not".

Wire-format caveat

Any existing [CanBeNull] collection with raw-serializable elements already stores the per-element flag, so changing propagation is a wire-format change for those types. In ModernUO at least PuzzleChest._guesses (Dictionary<Mobile, PuzzleChestSolutionAndTime>) is affected; List<Mobile> / HashSet<Item> / List<Item> uses go through the entity rule and are unaffected. A fix therefore needs a version bump and MigrateFrom on those consumers, landed together with the generator release.

Proposed fix

Strip [CanBeNull] (and [Tidy], which is a collection concern) from the attributes handed to element/key/value rules. If nullable elements are ever wanted, add an explicit annotation for that ([CanBeNullElements] or similar) rather than inferring it from the collection's attribute. Add a snapshot fixture with a [CanBeNull] List<NestedSerializable> pinning the single-flag output, and bump the affected ModernUO types in the same release.

Found while working on modernuo/ModernUO#2621, which avoids the annotation for now.

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions