Skip to content

Conversation

@gabriel-samfira
Copy link
Member

This PR is an attempt to move away from the thin-wrapper mode we've been operating under for the past 10 years (time flies). In this PR we add a number of important things:

  • Backwards compatibility is maintained. If you don't care about these changes, the commandlets should work as they always have.
  • The ability to serialize to PSCustomObjects. This has been requested a couple of times. It has some limitations, but for the most part, aside from a few edge cases, it will allow us to properly round-trip a YAML. One important limitation of PSCustomObjects is that it does not handle case sensitive keys. So if you have a key called Test and another called test in your YAML, only one key ca be stored in a PSCustomObject.
  • The ability to use powershell classes as a "model" for your YAML. This mode gives you the most flexibility out of the 3 modes.

PSCustomObjects

PSCustomObjects get you most of what class based models can, but without having to create a bunch of classes and model each and every property inside of them (although it is a good idea if you really want to be type safe and have proper round-tripping).

Here is a quick example of how the module works now with PSCustomObjects.

Define a Yaml:

PS /powershell-yaml> $yaml = @"                         
# Application configuration
app-name: "MyApp"
version: '1.0.0'
environment: production

# Database connection settings
database:
  host: db.example.com
  port: !!int 5432
  database: myapp_db
  username: app_user
  use-ssl: true

max-connections: !!int "100"
allowed-origins:
  - https://app.example.com
  - https://api.example.com
"@

Convert to PSCustomObject:

PS /powershell-yaml> $data = ConvertFrom-Yaml $yaml -as ([PSCustomObject])

Print out the object:

PS /powershell-yaml> $data

app-name          : MyApp
version           : 1.0.0
environment       : production
database          : @{host=db.example.com; port=5432; database=myapp_db; username=app_user; use-ssl=True; __psyaml_metadata=PowerShellYaml.Module.YamlMetadataStore}
max-connections   : 100
allowed-origins   : {https://app.example.com, https://api.example.com}
__psyaml_metadata : PowerShellYaml.Module.YamlMetadataStore

Convert back to yaml:

PS /powershell-yaml> ConvertTo-Yaml $data
# Application configuration
app-name: "MyApp"
version: '1.0.0'
environment: production
# Database connection settings
database:
  host: db.example.com
  port: !!int 5432
  database: myapp_db
  username: app_user
  use-ssl: true
max-connections: !!int "100"
allowed-origins:
- https://app.example.com
- https://api.example.com

Check types of some of the keys. Bellow you can see that both the port which has a scalar style of Plain and the max-connections that has a scalar style of DoubleQuoted, are cast to int32.

PS /powershell-yaml> $data.database.port.GetType()                        

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Int32                                    System.ValueType

PS /powershell-yaml> $data.'max-connections'.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Int32                                    System.ValueType

You will notice a couple of important things:

  • Comments are preserved when round tripping
  • Tags are preserved
  • Where tags are present, we don't infer the type. We cast it to the type dictated by the tag

To work with PSCustomObjects, we also added some helper commandlets that allow you to change comments and scalar styles:

Set-YamlPropertyScalarStyle -InputObject $data.database -PropertyName 'host' -Style DoubleQuoted
Set-YamlPropertyComment -InputObject $data.database -PropertyName "host" -Comment "This is the database server address"

Now if we serialize again:

PS /powershell-yaml> ConvertTo-Yaml $data                                                                                                  
# Application configuration
app-name: "MyApp"
version: '1.0.0'
environment: production
# Database connection settings
database:
  # This is the database server address
  host: "db.example.com"
  port: !!int 5432
  database: myapp_db
  username: app_user
  use-ssl: true
max-connections: !!int "100"
allowed-origins:
- https://app.example.com
- https://api.example.com

You can also get the value of the comment:

PS /powershell-yaml> Get-YamlPropertyComment -InputObject $data.database -PropertyName 'host'
This is the database server address

Powershell Classes

Most statically typed languages have some way to model the yaml so that you can reliably unmarshal the data in their proper types, save metadata and round-trip back to yaml. We can do something similar in powershell as well.

To that end, we have a number of new base classes and attributes we can use:

  • YamlConverter - a base class that allow you to implement custom marshaling/unmarshaling of tagged scalars
  • YamlBase - all classes that are meant to be marshaled via the -As flag of ConvertFrom-Yaml need to inherit from this class
  • YamlProperty (attribute) - An attribute that allows you to explicitly map a yaml key to a class property of your choice. This enables you to work around the case insensitive nature of PowerShell, define your attributes whichever way you want and have your models obey your coding style rather than rely on inference.

There is one quirk with classes I want to address early on. We define these base classes in the PowerShellYaml namespace. Once you import the powershell-yaml module, that namespace becomes available.

To use a namespace in PowerShell, you will use the using stanza. The problem is that any using stanzas you use, need to be the first thing in any script. So there is a chicken and egg situation happening here. As a result, we need to define our classes in a separate file and dot-source them.

For example, first we define our classes.ps1:

using namespace PowerShellYaml

class DatabaseConfig : YamlBase {
    [string]$Host = 'localhost'
    [int]$Port = 5432
    [string]$Database = ''
    [string]$Username = ''
    [bool]$UseSsl = $true
}

class AppConfig : YamlBase {
    [string]$AppName = ''
    [string]$Version = ''
    [string]$Environment = 'development'
    [DatabaseConfig]$Database = $null
    [int]$MaxConnections = 100
    [string[]]$AllowedOrigins = @()
}

Then we define our test.ps1:

# This will load our assemblies, commandlets, etc
Import-Module powershell-yaml

# Now that the PowerShellYaml namespace is available, we can dot-source the classes.ps1 file
. "$PSScriptRoot/classes.ps1"

$yaml = @"
# Application configuration
app-name: "MyApp"
version: '1.0.0'
environment: production

# Database connection settings
database:
  host: db.example.com
  port: !!int 5432
  database: myapp_db
  username: app_user
  use-ssl: true

max-connections: !!int "100"
allowed-origins:
  - https://app.example.com
  - https://api.example.com
"@

# Now we can convert our yaml to our powershell class
$config = ConvertFrom-Yaml $yaml  -As ([AppConfig])

If you print out the value:

PS /powershell-yaml> $config

AppName        : MyApp
Version        : 1.0.0
Environment    : production
Database       : DatabaseConfig
MaxConnections : 100
AllowedOrigins : {https://app.example.com, https://api.example.com}

To change the comment on a key:

# Warning: Case of the property name, matters
$config.Database.SetPropertyComment("Host", "This is the databse address")
$config.Database.SetPropertyScalarStyle("Host", "DoubleQuoted")

Now if we serialize back to yaml:

PS /powershell-yaml> ConvertTo-Yaml $config                                         
# Application configuration
app-name: "MyApp"
version: '1.0.0'
environment: production
# Database connection settings
database:
  # This is the databse address
  host: "db.example.com"
  port: !!int 5432
  database: myapp_db
  username: app_user
  use-ssl: True
max-connections: !!int "100"
allowed-origins:
- https://app.example.com
- https://api.example.com

There are a lot more knobs and features, most of which can be seen in action in the examples folder.

Add a strictly typed, round-trip capable parser
Use LoadFile even for typed implementation
Implement custom tags
Remove commandlet wrappers
Flow style and round tripping fixes

Signed-off-by: Gabriel Adrian Samfira <[email protected]>
Signed-off-by: Gabriel Adrian Samfira <[email protected]>
Signed-off-by: Gabriel Adrian Samfira <[email protected]>
@gabriel-samfira gabriel-samfira merged commit 9664752 into cloudbase:master Jan 5, 2026
6 checks passed
@gabriel-samfira gabriel-samfira deleted the models branch January 5, 2026 20:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant