When you need to pass the data of an object, in all its hierarchical intricacy, to another process, or save it to storage, there was, until recently, no alternative to XML for ‘serialization’ other than ASN.1 (Abstract Syntax Notation One). However, every computer language has a way of describing and representing data structures. PowerShell has its own terse, but powerful, style of object notation, richer than JSON, and easier to comprehend than XML. Why, I thought, was there no ConvertTo-PSON (PowerShell Object Notation) that would give us the means to get the PowerShell script for the data from objects, in a format that is capable of recreating the object, data-wise; Just like JSON.stringify()?. Why not, when one comes to think about it, is there no ConvertTo-YAML
to make is easier to inspect this data as if it were in a hierarchical list?
‘Why not?’, I muttered, as I strode to the keyboard.
Object Notation
Any data object, in the same way as a database table, is pretty useless without the means to easily get the data into or out of it. To transfer object data across a network, or to save it in a database or file, it has to be ‘serialized’ into a representation of its object hierarchy, generally in XML. When it is rehydrated, or ‘de-serialized’, the reverse process recreates the object hierarchy. In PowerShell, this sometimes happens under the covers when accessing remote object data.
We are most familiar with JSON as an object notation, because it is increasingly used for data exchange and storage, but it started life as being almost 100% standard JavaScript. It was valuable because it was so easy to ‘de-serialize’ because one could, if one was feeling reckless, merely execute it as JavaScript code. You can produce JSON in JavaScript with JSON.stringify()
function. JavaScript isn’t the only language where one can pull this trick: Every .NET language has its own way of representing the data within objects for the purposes of construction or persistence. PowerShell is no exception: Instead of the JSON array notation ‘[‘ and ‘]’, you have ‘@{‘ and ‘}’, and the ‘{‘ ‘}’ blocks need a ‘@ in the front ‘@{‘ ‘}’, to make it into a hash table. The colon ‘:’ becomes the assignment operator ‘=’. (<Pedantry> PowerShell officially doesn’t have an array notation, the @( … ) is an array sub-expression.</pedantry>)
I used the term ‘reckless’ to describe the old habits of executing Object Notations. JSON and PSON (and to a lesser extent YAML) have a fundamental security weakness. Although a JSON document could merely be executed in order to create the JavaScript object, any JavaScript would be executed. This would be an opportunity for a malicious hacker to get code executed. The same is true of PSON. You just execute it with invoke-expression. It is just too easy to slip in malicious PowerShell code. With JavaScript they closed the exploit by adding JSON.parse()
, which is now in the ECMA-262 standard. I know of no way of doing this with PSON.
I put in a rabbit-proof fence of Regex before doing anything like this.
PowerShell’s PSON (PowerShell Object Notation) is more comprehensive than JSON in that it allows you to specify the datatype, and allows many more datatypes. YAML, by contrast, was designed to be as easy as possible for humans to read, but it has made it hard to create a parser for it.
So, the classic JSON example
1 2 3 4 5 6 7 8 9 10 |
"menu": {"id": "file", "value": "File", "popup": { "menuitem": [ {"value": "New", "onclick": "CreateNewDoc()"}, {"value": "Open", "onclick": "OpenDoc()"}, {"value": "Close", "onclick": "CloseDoc()"} ] } } |
… becomes the similar-looking PowerShell equivalent …
1 2 3 4 5 6 7 8 9 10 11 |
@{ 'menu'= [ordered]@{"id"= "file"; 'value'= "File"; 'popup'= @{ "menuitem"= @( @{"value"= "New"; "onclick"= "CreateNewDoc()"}, @{"value"= "Open"; "onclick"= "OpenDoc()"}, @{"value"= "Close"; "onclick"= "CloseDoc()"} ) } } |
…as you will see if you then pass it through ConvertTo-JSON -depth 4 to get...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
{ "menu": { "id": "file", "value": "File", "popup": { "menuitem": [ { "onclick": "CreateNewDoc()", "value": "New" }, { "onclick": "OpenDoc()", "value": "Open" }, { "onclick": "CloseDoc()", "value": "Close" } ] } } } |
In YAML, this becomes
1 2 3 4 5 6 7 8 9 10 11 12 |
--- menu: id: file value: File popup: menuitem: - value: New onclick: CreateNewDoc() - value: Open onclick: OpenDoc() - value: Close onclick: CloseDoc() |
There are other good reasons for using YAML as well. I use it for embedding information in routines and procedures, it is excellent since it can be read easily, and updated automatically. It is great for document headers for the same reason. YAML is just so close to existing conventions for writing structured information that it has many uses. PowerShell shouldn’t be without it. To de-serialize YAML, I use YAML.NET.
Taking it for a spin.
Lets just see what a SQL Server table looks like in YAML. We’ll just grab a table in PowerShell and examine a few rows.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
$SourceTable= 'MyTable' $Sourceinstance='MyInstance' $Sourcedatabase='MyDatabase' try { $SourceConnectionString = "Data Source=$Sourceinstance;Initial Catalog=$Sourcedatabase;Integrated Security=True" $sql = "select top 20 * FROM $SourceTable" $SqlConnection = new-object System.Data.SqlClient.SqlConnection $SqlConnection.ConnectionString = $SourceConnectionString $SqlCommand = $SqlConnection.CreateCommand() $SqlCommand.CommandText = $sql $DataAdapter = new-object System.Data.SqlClient.SqlDataAdapter $SqlCommand $dataset = new-object System.Data.Dataset $DataAdapter.Fill($dataset) ConvertTo-YAML $dataset.Tables[0] } catch { $ex = $_.Exception Write-Error "whilst opening source $Sourceinstance . $Sourcedatabase . $SourceTable : Error'$($_)' in script $($_.InvocationInfo.ScriptName) $($_.InvocationInfo.Line.Trim()) (line $($_.InvocationInfo.ScriptLineNumber)) char $($_.InvocationInfo.OffsetInLine) executing $($_.InvocationInfo.MyCommand) " } |
Here is just one row from AdventureWorks contact table.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
- ModifiedDate: 2005-05-16T16:33:33 LastName: 'Ackerman' rowguid: 'df1fb8ab-2323-4330-9ab8-54e13ce6d8f9' MiddleName: '' EmailPromotion: 0 EmailAddress: 'pilar1@adventure-works.com' PasswordSalt: '/RPjvXw=' Suffix: '' Title: 'Sra.' PasswordHash: 'cql0lRWe1D/voQgg+XSLgdjSKgHBuM1DonTpX9ru0x8=' ContactID: 5 FirstName: 'Pilar' AdditionalContactInfo: > <AdditionalContactInfo xmlns="http://schemas.microsoft.com/sqlserver/2004/07/adv enture-works/ContactInfo" xmlns:crm="http://schemas.microsoft.com/sqlserver/2004 /07/adventure-works/ContactRecord" xmlns:act="http://schemas.microsoft.com/sqlse rver/2004/07/adventure-works/ContactTypes"><crm:ContactRecord date="2002-01-01Z" >Sales contacted this customer for the first time at<act:telephoneNumber><act:nu mber>432-4444</act:number></act:telephoneNumber>We talked about the Road bike price drop and the new spring models. Customer provided us new mobile number<act:mob ile><act:number>432-555-7809</act:number></act:mobile></crm:ContactRecord></Addi tionalContactInfo> NameStyle: 'False' Phone: '1 (11) 500 555-0132' |
…and here are the first five rows from the production.Location table in PowerShell (PSON) instead.
1 2 3 4 5 6 7 8 9 10 11 12 |
@( @{ 'CostRate' = 0.0000 ; 'ModifiedDate' = 1998-06-01T00:00:00 ; 'Name' = 'Tool Crib' ; 'Availability' = 0.00 ; 'LocationID' = 1 } , @{ 'CostRate' = 0.0000 ; 'ModifiedDate' = 1998-06-01T00:00:00 ; 'Name' = 'Sheet Metal Racks' ; 'Availability' = 0.00 ; 'LocationID' = 2 } , @{ 'CostRate' = 0.0000 ; 'ModifiedDate' = 1998-06-01T00:00:00 ; 'Name' = 'Paint Shop' ; 'Availability' = 0.00 ; 'LocationID' = 3 } , @{ 'CostRate' = 0.0000 ; 'ModifiedDate' = 1998-06-01T00:00:00 ; 'Name' = 'Paint Storage' ; 'Availability' = 0.00 ; 'LocationID' = 4 } , @{ 'CostRate' = 0.0000 ; 'ModifiedDate' = 1998-06-01T00:00:00 ; 'Name' = 'Metal Storage' ; 'Availability' = 0.00 ; 'LocationID' = 5 } ) |
Here is a bit of RSS feed rendered the same way. (the PowerShell script is in the header of the article).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
Feed: 'SQLServerCentral.com Articles tagged Editorial' Title: 'Challenge Yourself' Description: > How do you grow your career? Steve Jones has a few ideas on what you can do to both improve your skills and build your brand. Publication: 'SQL Server Central' Stream: 'Editorials' PageURL: 'http://www.sqlservercentral.com/Articles/Editorial' PubDate: 2014-09-30T06:00:00 author: author=null Ago: '16 days ago' link: 'http://www.sqlservercentral.com/articles/Editorial/116527/' --- Feed: 'SQLServerCentral.com Articles tagged Editorial' Title: 'Database Checkups' Description: > It's important that you are watching your databases' health to be sure that you can make changes, as well as rollback patches when issues occur. Publication: 'SQL Server Central' Stream: 'Editorials' PageURL: 'http://www.sqlservercentral.com/Articles/Editorial' PubDate: 2014-09-29T06:00:00 author: author=null Ago: '17 days ago' link: 'http://www.sqlservercentral.com/articles/Editorial/116526/' |
Creating a ConvertTo Cmdlet.
There was once only one available object-notation conversion in PowerShell and that was to XML using ConvertTo-XML
. You can access XML directly using dot-notation. To create JSON output from a PowerShell object, you can now use the built-in ConvertTo-JSON
, and there is a symmetrical ConvertFrom-JSON.
There is no equivalent ConvertTo-PSON
or ConvertTo-YAML
, but It is comparatively easy to create them. Theoretically, you don’t need a ConvertFrom-PSON
because you can just execute the string as a scriptblock though in reality it would be wise to have one to prevent a security loophole. You can convert from YAML with YAML.NET.
Here is a quick demo that shows a PowerShell object being converted into a string, which is then executed and finally turned into JSON to check that nothing got lost!
1 2 3 4 5 6 7 8 9 10 |
@{"menu"= @{ "popup"= @{ "menuitem"= @( #Array of hashes @{"value"= "New"; "onclick"= 'CreateNewDoc()'}, @{"value"= "Open"; "onclick"= 'OpenDoc()'}, @{"value"= "Close"; "onclick"= 'CloseDoc()'} ) } } } | ConvertTo-PSON |invoke-expression |ConvertTo-JSON -Depth 5 |
Since the built-in ConvertTo-XML
and ConvertTo-JSON
both do a lot of work to produce their object notation objects, it would seem, at first glance, wisest to just use these, with some clever Regex Strings to subsequently interpret into PowerShell object notation. However, although you can get quick results this way, it starts to get complicated when you try to refine it: You are limited in what you can do with PowerShell data types such as script blocks and XML. Likewise, you can implement a ConvertTo-YAML
very quickly using YamlNet
or SharpYAML
but YAML can be represented in a number of ways (it is effectively a superset of JSON
so you could use ConvertTo-JSON
!) and it is likely you’ll want more control. (I use Scott Muc’s PowerYaml
)
In creating these Cmdlets, I chose, instead, to use a recursive routine that could keep a count of the recursion level for formatting purposes, and which iterated through the arrays and hash tables using ForEach
. This gives a lot of freedom in choosing how the various types of data are displayed. You can use a strict ‘canonical’ form that specifies the datatype to avoid ambiguity, or use a looser, more readable form. You can indent the code as you wish. Just alter the function to taste.
The only real difficulty I hit was in dealing with the typical PowerShell objects such as Process Thread Collections
, or SMO database classes. It is easy got get this information, held as properties, but there are huge numbers of them. There is little consistency in the way the various Cmdlets deal with this.
Try this just to get a flavour of the enormous amount of data in just the first contained objects of a System.Diagnostics.Process
object , try this..
1 2 |
$xml=Get-process wi* | ConvertTo-XML $xml.OuterXml |
Even if you could, It is most unlikely that you’d need them all since many are obsolete. I ended up reckoning that, for ConvertTo-PSON
and ConvertTo-YAML
that it is better to specify the parameters you’re interested in, using the S
1 |
Get-Process wi* | Select-Object Handles,NPM,PM,WS,VM,CPU,Id,ProcessName | ConvertTo-PSON |
Just to show you that I’m no wimp, I settled on an algorithm that prevented any further recursion on a property of a complex object, and merely told you what the value was if it is a simple leaf value, or else the type of object it is. This is what ConvertTo-XML
does. This still makes the lights dim when it hits something like an SMO Database or Server object. It is much better to tell it what you want via Select-Object.
. You can render the object represented by the XML or you can do it just as an InnerXML string. The former method allows us to convert XML files directly into YAML or PSON. Useful? Sure. It is easier to demo than explain. First rendering it as an object…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
$xml= [Xml] @' <emp id="salary="'60000'> <name> <first>William</first> <last>Murphy</last> </name> <spouse> <name> <first>Cecilia Bertha matilda</first> <last>Murphy</last> </name> </spouse> <dept id="K55""'>Finance</dept> </emp> '@ ConvertTo-YAML $xml |
…giving …
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
--- emp: dept: #text: 'Finance' id: 'K55' id: '12345' name: first: 'William' last: 'Murphy' salary: '60000' spouse: name: first: 'Cecilia Bertha matilda' last: 'Murphy' |
and the other way, simply giving it as an XML fragment, (note the parameter if you want that behaviour) is.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
$xml= [Xml] @' <emp id="salary="'60000'> <name> <first>William</first> <last>Murphy</last> </name> <spouse> <name> <first>Cecilia Bertha matilda</first> <last>Murphy</last> </name> </spouse> <dept id="K55""'>Finance</dept> </emp> '@ ConvertTo-YAML $xml -XMLAsInnerXML 1 |
… giving …
1 2 3 4 |
--- | <emp id="salary=""60000"><name><first>William</first><last>Murphy</last></name><spouse><name><first>Cecilia Bertha matilda</first><last>Murphy</last></name></spouse><dept id="K55"">Finan ce</dept></emp> |
So, here is the basic function to produce YAML from a PowerShell object, ranging from an integer to a complex object.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 |
function ConvertTo-YAML { <# .SYNOPSIS creates a YAML description of the data in the object .DESCRIPTION This produces YAML from any object you pass to it. It isn't suitable for the huge objects produced by some of the cmdlets such as Get-Process, but fine for simple objects .EXAMPLE $array=@() $array+=Get-Process wi* | Select-Object Handles,NPM,PM,WS,VM,CPU,Id,ProcessName ConvertTo-YAML $array .PARAMETER Object the object that you want scripted out .PARAMETER Depth The depth that you want your object scripted to .PARAMETER Nesting Level internal use only. required for formatting #> [CmdletBinding()] param ( [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] [AllowNull()] $inputObject, [parameter(Position = 1, Mandatory = $false, ValueFromPipeline = $false)] [int]$depth = 16, [parameter(Position = 2, Mandatory = $false, ValueFromPipeline = $false)] [int]$NestingLevel = 0, [parameter(Position = 3, Mandatory = $false, ValueFromPipeline = $false)] [int]$XMLAsInnerXML = 0 ) BEGIN { } PROCESS { If ($inputObject -eq $null -and !($inputObject -ne $null)) { $p += 'null'; return $p } # if it is null return null if ($NestingLevel -eq 0) { '---' } $padding = [string]' ' * $NestingLevel # lets just create our left-padding for the block try { $Type = $inputObject.GetType().Name # we start by getting the object's type if ($Type -ieq 'Object[]') { $Type = "$($inputObject.GetType().BaseType.Name)" } #what it really is if ($depth -ilt $NestingLevel) { $Type = 'OutOfDepth' } #report the leaves in terms of object type elseif ($Type -ieq 'XmlDocument' -or $Type -ieq 'XmlElement') { if ($XMLAsInnerXML -ne 0) { $Type = 'InnerXML' } else { $Type = 'XML' } } # convert to PS Alias # prevent these values being identified as an object if (@('boolean', 'byte', 'byte[]', 'char', 'datetime', 'decimal', 'double', 'float', 'single', 'guid', 'int', 'int32', 'int16', 'long', 'int64', 'OutOfDepth', 'RuntimeType', 'PSNoteProperty', 'regex', 'sbyte', 'string', 'timespan', 'uint16', 'uint32', 'uint64', 'uri', 'version', 'void', 'xml', 'datatable', 'Dictionary`2', 'SqlDataReader', 'datarow', 'ScriptBlock', 'type') -notcontains $type) { if ($Type -ieq 'OrderedDictionary') { $Type = 'HashTable' } elseif ($Type -ieq 'PSCustomObject') { $Type = 'PSObject' } # elseif ($Type -ieq 'List`1') { $Type = 'Array' } elseif ($inputObject -is "Array") { $Type = 'Array' } # whatever it thinks it is called elseif ($inputObject -is "HashTable") { $Type = 'HashTable' } # for our purposes it is a hashtable elseif (($inputObject | gm -membertype Properties | Select name | Where name -like 'Keys') -ne $null) { $Type = 'generic' } #use dot notation elseif (($inputObject | gm -membertype Properties | Select name).count -gt 1) { $Type = 'Object' } } write-verbose "$($padding)Type:='$Type', Object type:=$($inputObject.GetType().Name), BaseName:=$($inputObject.GetType().BaseType.Name) " switch ($Type) { 'ScriptBlock'{ "{$($inputObject.ToString())}" } 'InnerXML' { "|`r`n" + ($inputObject.OuterXMl.Split("`r`n") | foreach{ "$padding$_`r`n" }) } 'DateTime' { $inputObject.ToString('s') } # s=SortableDateTimePattern (based on ISO 8601) using local time 'Byte[]' { $string = [System.Convert]::ToBase64String($inputObject) if ($string.Length -gt 100) { # right, we have to format it to YAML spec. '!!binary "\' + "`r`n" # signal that we are going to use the readable Base64 string format $bits = @(); $length = $string.Length; $IndexIntoString = 0; $wrap = 100 while ($length -gt $IndexIntoString + $Wrap) { $padding + $string.Substring($IndexIntoString, $wrap).Trim() + "`r`n" $IndexIntoString += $wrap } if ($IndexIntoString -lt $length) { $padding + $string.Substring($IndexIntoString).Trim() + "`r`n" } else { "`r`n" } } else { '!!binary "' + $($string -replace '''', '''''') + '"' } } 'Boolean' { "$(&{ if ($inputObject -eq $true) { 'true' } Else { 'false' } })" } 'string' { $String = "$inputObject" if ($string -match '[\r\n]' -or $string.Length -gt 80) { # right, we have to format it to YAML spec. $folded = ">`r`n" # signal that we are going to use the readable 'newlines-folded' format $string.Split("`n") | foreach { $length = $_.Length; $IndexIntoString = 0; $wrap = 80 while ($length -gt $IndexIntoString + $Wrap) { $breakpoint = $wrap $earliest = $_.Substring($IndexIntoString, $wrap).LastIndexOf(' ') $latest = $_.Substring($IndexIntoString + $wrap).IndexOf(' ') if (($earliest -eq -1) -or ($latest -eq -1)) { $breakpoint = $wrap } elseif ($wrap - $earliest -lt ($latest)) { $BreakPoint = $earliest } else { $BreakPoint = $wrap + $latest } if (($wrap - $earliest) + $latest -gt 30) { $BreakPoint = $wrap } # in case it is a string without spaces $folded += $padding + $_.Substring($IndexIntoString, $BreakPoint).Trim() + "`r`n" $IndexIntoString += $BreakPoint } if ($IndexIntoString -lt $length) { $folded += $padding + $_.Substring($IndexIntoString).Trim() + "`r`n`r`n" } else { $folded += "`r`n`r`n" } } $folded } else { "'$($string -replace '''', '''''')'" } } 'Char' { "([int]$inputObject)" } { @('byte', 'decimal', 'double', 'float', 'single', 'int', 'int32', 'int16', ` 'long', 'int64', 'sbyte', 'uint16', 'uint32', 'uint64') -contains $_ } { "$inputObject" } # rendered as is without single quotes 'PSNoteProperty' { "$(ConvertTo-YAML -inputObject $inputObject.Value -depth $depth -NestingLevel ($NestingLevel + 1))" } 'Array' { "$($inputObject | ForEach { "`r`n$padding- $(ConvertTo-YAML -inputObject $_ -depth $depth -NestingLevel ($NestingLevel + 1))" })" } 'HashTable'{ ("$($inputObject.GetEnumerator() | ForEach { "`r`n$padding $($_.Name): " + (ConvertTo-YAML -inputObject $_.Value -depth $depth -NestingLevel ($NestingLevel + 1)) })") } 'Dictionary`2'{ ("$($inputObject.GetEnumerator() | ForEach { "`r`n$padding $($_.Key): " + (ConvertTo-YAML -inputObject $_.Value -depth $depth -NestingLevel ($NestingLevel + 1)) })") } 'PSObject' { ("$($inputObject.PSObject.Properties | ForEach { "`r`n$padding $($_.Name): " + (ConvertTo-YAML -inputObject $_ -depth $depth -NestingLevel ($NestingLevel + 1)) })") } 'generic' { "$($inputObject.Keys | ForEach { "`r`n$padding $($_): $(ConvertTo-YAML -inputObject $inputObject.$_ -depth $depth -NestingLevel ($NestingLevel + 1))" })" } 'Object' { ("$($inputObject | Get-Member -membertype properties | Select-Object name | ForEach { "`r`n$padding $($_.name): $(ConvertTo-YAML -inputObject $inputObject.$($_.name) -depth $NestingLevel -NestingLevel ($NestingLevel + 1))" })") } 'XML' { ("$($inputObject | Get-Member -membertype properties | where-object { @('xml', 'schema') -notcontains $_.name } | Select-Object name | ForEach { "`r`n$padding $($_.name): $(ConvertTo-YAML -inputObject $inputObject.$($_.name) -depth $depth -NestingLevel ($NestingLevel + 1))" })") } 'DataRow' { ("$($inputObject | Get-Member -membertype properties | Select-Object name | ForEach { "`r`n$padding $($_.name): $(ConvertTo-YAML -inputObject $inputObject.$($_.name) -depth $depth -NestingLevel ($NestingLevel + 1))" })") } # 'SqlDataReader'{$all = $inputObject.FieldCount; while ($inputObject.Read()) {for ($i = 0; $i -lt $all; $i++) {"`r`n$padding $($Reader.GetName($i)): $(ConvertTo-YAML -inputObject $($Reader.GetValue($i)) -depth $depth -NestingLevel ($NestingLevel+1))"}} default { "'$inputObject'" } } } catch { write-error "Error'$($_)' in script $($_.InvocationInfo.ScriptName) $($_.InvocationInfo.Line.Trim()) (line $($_.InvocationInfo.ScriptLineNumber)) char $($_.InvocationInfo.OffsetInLine) executing $($_.InvocationInfo.MyCommand) on $type object '$($inputObject)' Class: $($inputObject.GetType().Name) BaseClass: $($inputObject.GetType().BaseType.Name) " } finally { } } END { } } |
How it works.
ConvertTo-YAML
and ConvertTo-PSON
are almost identical. There are just to many differences to make it worth trying to fold it all into one function but it should give you a start on how to do a ConvertTo-CSharp
or ConvertTo-VB.
This is a recursive function that just returns the representation of whatever object is passed to it. If it is a simple data value, then it is rendered according to the rules for the notation. The only difficulties come with strings, XML, bitmaps and so on. If it is a complex object, then there are a number of ways it can be shredded to get at the individual elements. Where the object is a hashtable of array, one merely needs to pass it through a pipeline that explores and returns the branch. There could be all sorts of nested arrays and hashtables in there. Different types of objects require different treatments, but the various alternative pipelines are somewhat similar.
The function is done as an advanced PowerShell function so it can be used in a pipeline as well. Where possible, I’ve made it work in a similar way to ConvertTo-JSON
. The only difference is in the handling of complex objects, where ConvertTo-JSON
and ConvertTo-XML
relies on the -depth parameter to stop you from returning huge elaborate structures. I preferred just to handle them by stopping at the first level at which it found properties in complex objects. (Depth=1). I’m uneasy about NET properties and the way they’re abused. Properties generally supposed to expose the internal state of an object, which is what we want. Functions, which we avoid, return a query, or modify the state. Unfortunately, in.NET, there are plenty of functions disguised as properties.
Different flavors of Object Notations.
Because of the different requirements of any object notation, it is difficult to package up a universal solution without a bewildering number of switch-parameters to specify what you want. It could be better to roll your own function based on a working ‘core’. One could, of course, go entirely the other way and produce a function that will serialize to any object notation and positively bristles with options. I’d rather not.
Formatting
There are many different ways of formatting JSON. The same is true of PSON, because the indenting is pure decoration. You can leave out whitespace entirely outside string literals to save space, or have formatting optimized to make it easy on the eye. Few will agree on the best way of doing this. YAML is different because indenting is used, Pythonesque, instead of bracketing to indicate nesting. YAML has its own creative outlets since it allows JSON as a subset of YAML. Lists can be bracketed and comma-delimited. Long strings can be represented in a number of different ways depending on how easily you’d like the YAML to be read. In fact, YAML allows so many different choices that the creation of a YSON deserialiser is extraordinarily difficult. Yes, there are probably too many options and preferences to make a simple built-in serialiser entirely useful.
Datatype definition
As well as formatting, both PowerShell and YAML allow a great deal of latitude in the way that a datatype is specified. In both cases, it can be left implied. It is reasonably easy to tell the difference between an integer and decimal number, and one can represent a float in a way that makes it obvious. Other datatypes are less obvious and can be specified. YAML has a canonical form that is more exact but more readable. PSON can, likewise, be liberally sprinkled with Type accelerators in square-brackets to specify the datatype unambiguously.
Object-Depth and hierarchical-depth
Even though .NET objects can contain simple hierarchies consisting of simple values, arrays and hashtables, they can also have contained objects, and so act more like graphs/networks. If you explore them in too much depth you can end up in all sorts of difficulties, particularly with loops. Simple properties can be disguised as functions. The built-in Cmdlets ConvertTo-XML
limits depth of contained objects arbitrarily to a set figure which prefents infinite looping. The default value is 1; Anything more than that produces a cacophony of useless information. Worse, one can get into an endless circular reference. ConvertTo-XML
has a proper understanding of what a contained object (Data structures like hashtables and arrays aren’t counted as contained objects)
1 2 3 4 5 6 7 8 9 |
$ratherNested=@{"FirstLevel"= @{ "SecondLevel"= @{ "ThirdLevel"= @{ "FourthLevel"= @('one','two','three') } } } } | ConvertTo-XML -depth 1 $ratherNested.OuterXml |
whic
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<?xml version="1.0" encoding="UTF-8"?> <Objects> <Object Type="System.Collections.Hashtable"> <Property Name="Key" Type="System.String">FirstLevel</Property> <Property Name="Value" Type="System.Collections.Hashtable"> <Property Name="Key" Type="System. String">SecondLevel</Property> <Property Name="Value" Type="System.Collections.Hashtable"> <Property Name="Key" Type="System.String">ThirdLevel</Property> <Property Name="Value" Type="System.Collections.Hashtable"> <Property Name="Key " Type="System.String">FourthLevel</Property> <Property Name="Value" Type="System.Object[]"> <Property Type="System.String">one</Property> <Property Type="System.String">two</Property> <Property Type="System.String">three</Property> </Property> </Property> </Property> </Property> </Object> </Objects> |
Whereas ConvertTo-JSON
has an entirely different concept of what a contained object is. It seems to have equated hierarchical nesting level with depth and produces an entirely different result.
1 2 3 4 5 6 7 8 |
@{"FirstLevel"= @{ "SecondLevel"= @{ "ThirdLevel"= @{ "FourthLevel"= @('one','two','three') } } } } | ConvertTo-JSON -depth 1 |
… producing …
1 2 3 4 5 |
{ "FirstLevel": { "SecondLevel": "System.Collections.Hashtable" } } |
In the ConvertTo-YAML
and -PSON
routines, I only iterate through the parameters values of complex objects as a last resort and then only as a leaf. I’ll describe the nature of the leaf but not shred it. This is equivalent to ConvertTo-XML
at level 1. This way I can make a reasonable representation of one of those objects without going too far into the details. There was a time I removed that constraint and kicked up the depth figure just to see what happened. I got the scrolling screen of terror, It looked for all the world as if half the Wikipedia and several binary images went scrolling up the console.
There is another reason for caution: Some SMO objects use lazy loading, and to iterate through all the possible data ‘within’ contained objects would bring the SQL Server instance to its knees. They were designed to be eaten in small bites.
More irritating for me than the byzantine complexity of many windows objects was seeing that the CMDlets such as Format-table are able to display just the more important property values of a contained object. Perhaps a kind reader can tell me how they know. At that point I felt there was enough complexity for an article.
The point of recounting all this is to illustrate the suggestion that there may be some virtue in ‘rolling your own’ object serialiser for special purposes.
Conclusions.
I hope I’ve shown that there is nothing hideously difficult in ‘stringifying’ object data into forms that can be consumed by other processes, or for the purposes of searching or exploring data.
I’ve had mixed motives for doing this. I wanted to show how to use PowerShell to read and write YAML in headers , and to use PSON to gain object persistence in PowerShell. I wanted to suggest a more economical way of passing PowerShell Objects across a network. I also wanted to hint that instead of using XML or JSON as a universal ‘esperanto’ data transfer format, one could use whatever format is native to the destination.
To take a human parallel, it is easier with data transfer to speak a foreign language than it is to understand one. To make this point, I’ve done two PowerShell functions to convert to PowerShell Object notation and YAML. To do it in Visual Basic or C# is easy. What about SQL? A ridiculous idea, you might think, but one can use the SELECT…VALUES statement to pass data, and one could represent a hierarchy in a hierarchy table.
I like the idea of databases being able to consume object data without the application developer having to feel responsible for the mental gymnastics required to convert the object viewpoint of data into the relational form. We currently have the technology to hand XML, in all its trickiness, through to the database to be shredded there into its relational data. Rather than repeat the process with other object notation formats, why not devise an intermediate form, a hierarchy table, that can be passed via its SQL object notation or a table-valued parameter to a stored procedure that performs the mystery of updating base tables with the data in a transactional way, as we once dreamed of with the updategram.
Load comments