Repository navigation
Validate objects inside items that are collections - #82
Merged
Merged
Conversation
An item that is itself a collection is validated as an object but never enumerated, so a List of List, a List of HashSet, or an object array that holds a List passes with invalid objects inside. Arrays and ArrayList are caught by accident through SyncRoot. The new specs state the fixed behavior and fail until the fix lands: index at each level (Value[0][0].Name), null items, repeated and cyclic collections, sealed and struct collections, and an item that has its own attributes. Guards pin what must keep working: attributes and IValidatableObject on a collection item, leaf collections not being enumerated, and a collection as the root object. Open cases are skipped with their reasoning in NestedCollectionEdgeCaseTests. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The validator enumerated a collection that a property holds, but never an item that is itself a collection. A List of List, a List of HashSet, or an object array that holds a List passed with invalid objects inside. Arrays and ArrayList were reached by accident through SyncRoot. TryValidateObjectRecursive now takes enumerateItems. An item that is a collection is validated as an object first, so its attributes and IValidatableObject still run, then its items are enumerated before its properties are walked. The path has the index of each level: Value[0][0].Name. A collection of leaf types is not enumerated. IsLeafType now treats a sealed class or struct that is a collection as a leaf only when it is a collection of leaf types. Without that, a List of ImmutableList or of a sealed collection was skipped as a list of values. A type that yields itself is answered as not a leaf, to end the check. A default ImmutableArray item is skipped, because enumerating it throws and it holds nothing. Any other item that throws when enumerated throws from validation, like a collection that a property holds. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A struct collection such as ImmutableArray is now validated as an item of another collection, but a property that holds it is still not read, because the validator walks reference-type properties only. The same invalid objects pass or fail depending on one level of nesting. A guard pins the gap. Skipped specs state the planned behavior for the next release, with the plan and the steps for converting the guards. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Mark the two changed member name formats as BREAKING, and add a Collections section to the README. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A class-level attribute that passes ValidationContext.MemberName, or a Validate method that yields a null member name, gives an item result with a null member name. Release 2.4.0 read the name to decide whether it was an index, so it threw NullReferenceException. A name that starts with "[" changed from "Value[0].[Totals]" to "Value[0][Totals]". The tests pin the paths of 2.3.3 for these names and state the path of a nested collection. They fail until the fix lands. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The validator joined the path of an item that is a collection by looking at the start of each member name. A null name threw NullReferenceException, and a name that starts with "[" lost its dot. A private ValidationResult subclass now marks the results whose names start with an item index, so the names the items choose are never inspected. A null name gives "Value[0]." as in 2.3.3. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The BREAKING entry now states the rule: an item that is a collection is enumerated before its properties are walked, so any path through a property of the item changed. List the verified examples. Add the lazy sequence, hang and stack depth notes, and fix the README claim about dictionary values that are structs. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The validator remembers objects by reference, and a boxed struct copy is never the same reference. A struct that two properties hold was already reported twice in 2.3.3. Since 2.4.0 an item collection that returns its structs by enumeration and through a property, such as ArraySegment, reports each one twice, and LinkedList three times. Limitation guards, so a later fix is a deliberate change. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A default ArraySegment, or a struct of your own that wraps a null array, threw when the item was enumerated, where 2.3.3 returned valid. The check for a default ImmutableArray found the type by name and covered only that one. IsDefaultStruct compares the struct to its default value with its own Equals, so it needs no list of types. Tests cover ImmutableArray, ArraySegment and a struct of the caller's own, boxed and unboxed, and the cases that must still be validated: a segment with an array, an empty segment, and a segment of bytes. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Add the gotcha that a struct reached by two routes is reported twice, with the way out for a collection of the caller's own. Say that any struct item that equals its default is skipped, not only ImmutableArray. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Replace the long BREAKING bullet with one BREAKING entry for the changed member names, with one line per shape, and one for enumeration side effects. Move the stack and known-limit notes to plain entries. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
5 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
The validator enumerated a collection that a property holds, but never an item that is itself a collection. A
List<List<T>>, aList<HashSet<T>>, anobject[]that holds a list, or a list ofImmutableList<T>passed validation even when an object inside was invalid. A probe of v2.0.0, v2.2.0 and v2.3.3 shows the same result in all three, so this is a long-standing silent pass. Some shapes were caught only by accident, through a public property of the inner collection (Array.SyncRoot,Dictionary.Values), with paths such asValue[0].SyncRoot[0].Name.The fix enumerates items that are collections and reports each level's index:
Value[0][0].Name. Paths are now the same for every collection type, which is why this is a breaking change for the few shapes that had an accidental path. By the rule in RELEASE-PROCESS.md it needs a major version, so I expect it to ship in 3.0.Summary
IValidatableObject.Validatestill run, then its items are enumerated before its properties are walked. A collection of simple values is not enumerated. The root object is not enumerated either, and that is pinned by tests.IsLeafTypenow treats a sealed class or struct that is a collection as a leaf only when its items are leaves. Without it,List<ImmutableList<T>>and lists of sealed collections were skipped as lists of values.ImmutableArray<T>orArraySegment<T>, is skipped, because enumerating it throws.Test plan
dotnet teston net8.0 and net10.0: 503 passed, 14 skipped, 0 failed. The 14 skipped include the next-release specs.dotnet build -p:IncludeNetFramework=truebuilds net481 with no warnings.