Nested Data, Copy Semantics, Equality, and Choosing a Data Structure
Combine arrays, dictionaries, and custom objects into nested configuration, understand shared references and shallow copies, define equality deliberately, and recognize when data needs a stronger application boundary.
Learning objectives
- Model multi-environment configuration with nested arrays, hashtables, and custom objects while keeping access paths understandable.
- Explain how assignment of mutable reference objects can make two variables point to the same instance.
- Distinguish shallow copies from independent nested state and choose explicit rebuilding or serialization only with known tradeoffs.
- Avoid assuming arbitrary objects use intuitive structural equality and compare explicit business keys when needed.
- Choose arrays, hashtables, PSCustomObject, classes, JSON/YAML, or databases based on required operations.
- Recognize when a PowerShell script is becoming a data-heavy application that needs dedicated persistence or query infrastructure.
1. Real configuration is often nested because environments contain related settings
A realistic deployment configuration rarely fits one flat dictionary. An environment can have a region, a list of servers, feature flags, and nested service settings. PowerShell can nest arrays, hashtables, and custom objects so the in-memory shape mirrors the domain.
$config = [ordered]@{
Application = 'catalog'
Environments = @{
staging = [pscustomobject]@{
Region = 'eu-west'
Servers = @('stage-api-01', 'stage-worker-01')
Features = @{ Canary = $true; Diagnostics = $true }
}
production = [pscustomobject]@{
Region = 'us-east'
Servers = @('prod-api-01', 'prod-worker-01')
Features = @{ Canary = $false; Diagnostics = $false }
}
}
}
$config.Environments['staging'].Servers[0]
$config.Environments['production'].Features['Canary']The access syntax follows the data shape: dictionary key, object property, array index. If the access path becomes unreadably deep, that is a design signal to create intermediate variables or a stronger abstraction.
2. Assignment can copy a value or copy a reference to the same mutable object
Simple scalar values such as integers and strings behave like independent values in ordinary assignment scenarios. Many mutable objects, including custom objects and nested collections, are reference types. Assigning a second variable can make both variables point to the same underlying object.
$firstNumber = 1
$secondNumber = $firstNumber
$secondNumber = 2
"$firstNumber / $secondNumber"
$firstServer = [pscustomobject]@{ Name='api-01'; Healthy=$true }
$secondServer = $firstServer
$secondServer.Healthy = $false
$firstServer.Healthy1 / 2
FalseThe second example surprises beginners because changing a property through $secondServer changes the object also reached through $firstServer. There was one object with two references, not two independent objects.
3. A shallow copy duplicates the outer object but can still share nested children
PowerShell custom objects expose PSObject.Copy(), which creates a shallow copy. Hashtables support Clone(). “Shallow” means the outer container is new, but nested reference-type values can still point to the same child objects.
$original = [pscustomobject]@{
Name = 'staging'
Features = @{ Canary = $true }
}
$copy = $original.PSObject.Copy()
$copy.Name = 'staging-copy'
$copy.Features['Canary'] = $false
$original.Name
$original.Features['Canary']The name remains independent because the outer object was copied. The nested hashtable is shared, so changing its Canary value is visible through both outer objects.
4. “Deep copy” is a policy decision, not one universal PowerShell operator
A deep copy recursively duplicates nested mutable state. There is no single PowerShell operator that guarantees the right deep-copy semantics for every object type. Serialization round-trips can sometimes produce independent data, but they may change types, methods, precision, or metadata. For configuration, the safest strategy is often to construct a new object from validated source values.
$source = [pscustomobject]@{
Name = 'staging'
Servers = @('stage-api-01', 'stage-worker-01')
Features = @{ Canary = $true }
}
$independent = [pscustomobject]@{
Name = $source.Name
Servers = @($source.Servers)
Features = @{
Canary = [bool]$source.Features['Canary']
}
}
$independent.Features['Canary'] = $false
$source.Features['Canary']This explicit rebuild is verbose but clear. Chapter 11 will cover JSON and CLIXML serialization and their type-fidelity tradeoffs.
5. Object equality is not automatically “same visible properties”
For simple scalars, equality is intuitive: 3 -eq 3 and "api" -eq "api" are true. For custom reference objects, two independently created records with the same visible properties should not be assumed to compare as structurally equal. If structural equality matters, define which properties constitute equality and compare those deliberately.
$a = [pscustomobject]@{ Name='api-01'; Port=8443 }
$b = [pscustomobject]@{ Name='api-01'; Port=8443 }
$sameReference = $a
$a -eq $b
$a -eq $sameReference
$sameShape = ($a.Name -eq $b.Name) -and ($a.Port -eq $b.Port)
$sameShapeDo not build critical deduplication logic on assumptions about how arbitrary objects implement equality. Decide the business key—perhaps Name plus Environment—and compare that key.
6. Containment operators inherit the equality semantics of the elements
-contains and -in are clear with scalar values such as strings and numbers. With custom objects, containment is only as intuitive as object equality. A newly created object with matching property text is not automatically the same element as an existing object reference.
$names = 'api-01', 'api-02'
$names -contains 'api-01'
'api-02' -in $names
$server = [pscustomobject]@{ Name='api-01' }
$servers = @($server)
$servers -contains $server
$servers -contains ([pscustomobject]@{ Name='api-01' })For record collections, search by a stable property or business key instead: $servers.Name -contains "api-01" or $servers | Where-Object Name -eq "api-01".
7. Choose a structure based on the operations the data must support
| Need | Good starting choice | Reason |
|---|---|---|
| Ordered sequence of values | Array | Preserves element order and supports indexing/enumeration. |
| Repeated append while constructing many items | List[T] | Resizable collection avoids repeated array reconstruction. |
| Named lookup/configuration map | Hashtable | Fast direct lookup by meaningful key. |
| Dictionary with intentional insertion order | [ordered] dictionary | Preserves key sequence as part of the contract. |
| Pipeline/report record | PSCustomObject | Stable named properties flow naturally through PowerShell commands. |
| Behavior plus stronger reusable type contract | PowerShell class | Useful when methods/invariants justify a defined type; Chapter 15. |
| Interchange/configuration file | JSON or YAML | Portable external representation; parser/schema rules still matter. |
| Large shared/queryable state | External database | Concurrency, indexing, durability, and query needs exceed in-memory script structures. |
The table is a starting heuristic, not a law. Ask what operations dominate: lookup, ordered traversal, repeated append, pipeline projection, persistence, concurrency, or querying.
8. JSON and YAML are external representations, not replacements for in-memory design
Configuration files are useful when data must live outside the script, move between tools, or be reviewed in source control. JSON and YAML encode data; after parsing, PowerShell still needs an intentional in-memory model. JSON support is built into PowerShell, while YAML commonly uses an external module or another tool. Chapter 11 covers these formats in depth.
$record = [pscustomobject]@{
Name = 'api-01'
Port = 8443
Healthy = $true
}
$json = $record | ConvertTo-Json
$roundTrip = $json | ConvertFrom-Json
$roundTrip | Get-MemberA serialization round-trip can change exact runtime types and custom metadata. Treat serialization as a boundary with an explicit schema expectation, not as a magic cloning mechanism.
9. Know when a PowerShell script is becoming a data-heavy application
Warning signs include many cross-linked records, concurrent writers, complex queries, migrations, transactions, long-lived mutable state, ad-hoc indexes implemented with nested hashtables, or large datasets repeatedly serialized wholesale. PowerShell can orchestrate an application that owns such data, but the application may need a database or dedicated service rather than ever-deeper in-memory dictionaries.
10. Model business meaning first, then choose PowerShell syntax
A useful design sequence is: identify the entities, identify which values are scalar versus repeated, identify lookup keys, decide which records need stable properties, and identify persistence/concurrency requirements. Only then choose arrays, dictionaries, custom objects, files, or a database. This prevents syntax familiarity from driving the model.
11. Diagnose nested-state bugs by tracing references and boundaries
When nested data changes unexpectedly, determine whether two variables reference the same object. [object]::ReferenceEquals() is useful for learning and diagnostics. Then inspect nested child references separately.
$original = [pscustomobject]@{
Name = 'staging'
Features = @{ Canary = $true }
}
$copy = $original.PSObject.Copy()
[object]::ReferenceEquals($original, $copy)
[object]::ReferenceEquals($original.Features, $copy.Features)False
TrueThat result captures shallow-copy semantics exactly: different outer objects, shared nested hashtable.
12. Design exercise: choose structures for a multi-environment deployment model
Read the requirements, implement one reasonable model, and then justify each structure. There can be more than one defensible design; the important skill is matching structures to operations.
- Three environments: dev, staging, production; code must look them up by name.
- Each environment has an ordered rollout list of server names.
- Each server record has Name, Role, Port, and Enabled properties and must be filterable/exportable.
- A small feature-flag map is read by key.
- The whole configuration will later be stored in a version-controlled file, but this lesson stays in memory.
$model = [ordered]@{
Application = 'catalog'
Environments = @{
dev = [pscustomobject]@{
Region = 'local'
Servers = @(
[pscustomobject]@{ Name='dev-api-01'; Role='api'; Port=8080; Enabled=$true }
)
Features = @{ Canary=$true }
}
staging = [pscustomobject]@{
Region = 'eu-west'
Servers = @(
[pscustomobject]@{ Name='stage-api-01'; Role='api'; Port=8443; Enabled=$true }
[pscustomobject]@{ Name='stage-worker-01'; Role='worker'; Port=9000; Enabled=$true }
)
Features = @{ Canary=$true }
}
production = [pscustomobject]@{
Region = 'us-east'
Servers = @(
[pscustomobject]@{ Name='prod-api-01'; Role='api'; Port=8443; Enabled=$true }
[pscustomobject]@{ Name='prod-worker-01'; Role='worker'; Port=9000; Enabled=$true }
)
Features = @{ Canary=$false }
}
}
}
$environment = $model.Environments['staging']
$enabledServers = @($environment.Servers | Where-Object Enabled)
$enabledServers | Select-Object Name, Role, Port
"Canary enabled: $($environment.Features['Canary'])"One justification: the environment map is a hashtable because lookup by name dominates; the server collection is an array because rollout order matters; each server is a custom object because it is a pipeline/report record; feature flags are a hashtable because direct key lookup is natural.
Verification checklist
13. Common mistakes to avoid
Assuming assignment clones mutable objects. Two variables can reference the same object.
Calling a shallow copy “independent” without checking nested state. Child collections may still be shared.
Assuming visible property equality means object equality. Define and compare the business key or required fields explicitly.
Using deeper nested hashtables to solve application-scale persistence. Choose a database or service when durability, concurrency, and querying become first-class requirements.
14. Knowledge check
Question 1. What can happen when you assign the same PSCustomObject to two variables?
Question 2. What does a shallow copy duplicate?
Question 3. Why should you not assume two separate PSCustomObjects with the same visible properties are structurally equal?
Question 4. When is an external database a better fit than nested in-memory PowerShell data?
Question 5. Why are JSON/YAML not substitutes for choosing an in-memory model?
15. Summary
Nested state combines the structures from this chapter, but reference semantics make copying and equality more subtle than visible output suggests. Assignment can create multiple references to one mutable object; shallow copies duplicate only the outer layer; deep-copy behavior must be chosen deliberately. Arrays, dictionaries, custom objects, classes, serialization formats, and databases each fit different operations. Good PowerShell automation models business meaning first and moves data to stronger persistence/query systems when script-local structures stop matching the problem.
16. Further reading
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this address.