-
Notifications
You must be signed in to change notification settings - Fork 5
/
ConvertTo-STJson.ps1
355 lines (279 loc) · 16.6 KB
/
ConvertTo-STJson.ps1
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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
# Author: Joakim Borger Svendsen, 2017.
# JSON info: http://www.json.org
# Svendsen Tech. MIT License. Copyright Joakim Borger Svendsen / Svendsen Tech. 2016-present.
# v0.3, 2017-04-12 (second release of the day, I actually read some JSON syntax this time)
# Fixed so you don't double-whack the allowed escapes from the diagram, not quoting null, false and true as values.
# v0.4. Scientific numbers are supported (not quoted as values). 2017-04-12.
# v0.5. Adding switch parameter EscapeAllowedEscapesToo (couldn't think of anything clearer),
# which also double-whacks (escapes with backslash) allowed escape sequences like \r, \n, \f, \b, etc.
# Still 2017-04-12.
# v0.6: It's after midnight, so 2017-04-13 now. Added -QuoteValueTypes that makes it quote null, true and false as values.
# v0.7: Changed parameter name from EscapeAllowedEscapesToo to EscapeAll (... seems obvious now). Best to do it before it's
# too late. 2017-04-13.
# v0.7.1: Made the +/- after "e" in numbers optional as this is apparently valid (as plus, then)
# v0.8: Added a -Compress parameter! 2017-04-13.
# v0.8.1: Fixed bug that made "x.y" be quoted (but scientific numbers and integers worked all the while). 2017-04-14.
# v0.8.2: Fixed bug with calculated properties (yay, this improves flexibility significantly). 2017-04-14.
# v0.9: Almost too many changes to mention. Now null, true and false as _value types_ are unquoted, otherwise they
# are quoted. Comparing to the PowerShell team's ConvertTo-Json. Now escaping works better and more
# standards-conforming. If you have a newline in the strings, it'll be replaced by "\n" (literally, not a newline),
# while if you have "\n" literally, it'll turn into \\n. Code quality improvements. Refactoring. Still some more to fix,
# but it's getting better. Datetime stuff is bothering me, not sure I like how it's handled in the PS team's cmdlet, but I
# don't have a sufficiently informed opinion.
#
# v0.9.1: Formatting fixes.
# v0.9.2: Returning proper value types when sending in only single values of $true and $false (passed through).
# $null is buggy, but only if you pass in _nothing_ else, but $null. As a value in an array, hash or
# anywhere else, it works fine.
# v0.9.2.1: Forgot.
# v0.9.2.2: Adding escaping of "solidus" (forward slash).
# v0.9.3: Coerce numbers from strings only if -CoerceNumberStrings is specified (non-default), properly detect numerical types and
# by default omit double quotes only on these.
# v0.9.3.1: Respect and do not doublewhack/escape (regex) "\u[0-9a-f]{4}".
# v0.9.3.2: Undoing previous change ... (wrong logic).
# v0.9.3.3: Comparing to the PS team's ConvertTo-Json again and they don't escape "/" alone. Undoing 0.9.2.2 change.
# v0.9.3.4: Support the IA64 platform and int64 on that too.
# v0.9.4.0: Fix nested array bracket alignment issues. 2017-10-21.
# v0.9.5.0: Handle NaN for [Double] so it's a string and doesn't break JSON syntax with "Nan" unquoted
# in the data.
# * Add the -DateTimeAsISO8601 switch parameter (causing datetime objects to be in this format:
# '2018-06-25T01:25:00').
# v0.9.5.1: Handle "infinity" as well for System.Double.
# v0.9.5.2: Fix bug with DateTime ISO formatting inside hash tables and PS objects.
# v1.0: Improve readability, by "popular" demand...
######################################################################################################
# Take care of special characters in JSON (see json.org), such as newlines, backslashes
# carriage returns and tabs.
# '\\(?!["/bfnrt]|u[0-9a-f]{4})'
function EscapeJson {
param(
[String] $String)
# removed: #-replace '/', '\/' `
# This is returned
$String -replace '\\', '\\' -replace '\n', '\n' `
-replace '\u0008', '\b' -replace '\u000C', '\f' -replace '\r', '\r' `
-replace '\t', '\t' -replace '"', '\"'
}
# Meant to be used as the "end value". Adding coercion of strings that match numerical formats
# supported by JSON as an optional, non-default feature (could actually be useful and save a lot of
# calculated properties with casts before passing..).
# If it's a number (or the parameter -CoerceNumberStrings is passed and it
# can be "coerced" into one), it'll be returned as a string containing the number.
# If it's not a number, it'll be surrounded by double quotes as is the JSON requirement.
function GetNumberOrString {
param(
$InputObject)
if ($InputObject -is [System.Byte] -or $InputObject -is [System.Int32] -or `
($env:PROCESSOR_ARCHITECTURE -imatch '^(?:amd64|ia64)$' -and $InputObject -is [System.Int64]) -or `
$InputObject -is [System.Decimal] -or `
($InputObject -is [System.Double] -and -not [System.Double]::IsNaN($InputObject) -and -not [System.Double]::IsInfinity($InputObject)) -or `
$InputObject -is [System.Single] -or $InputObject -is [long] -or `
($Script:CoerceNumberStrings -and $InputObject -match $Script:NumberRegex)) {
Write-Verbose -Message "Got a number as end value."
"$InputObject"
}
else {
Write-Verbose -Message "Got a string (or 'NaN') as end value."
"""$(EscapeJson -String $InputObject)"""
}
}
function ConvertToJsonInternal {
param(
$InputObject, # no type for a reason
[Int32] $WhiteSpacePad = 0)
[String] $Json = ""
$Keys = @()
Write-Verbose -Message "WhiteSpacePad: $WhiteSpacePad."
if ($null -eq $InputObject) {
Write-Verbose -Message "Got 'null' in `$InputObject in inner function"
$null
}
elseif ($InputObject -is [Bool] -and $InputObject -eq $true) {
Write-Verbose -Message "Got 'true' in `$InputObject in inner function"
$true
}
elseif ($InputObject -is [Bool] -and $InputObject -eq $false) {
Write-Verbose -Message "Got 'false' in `$InputObject in inner function"
$false
}
elseif ($InputObject -is [DateTime] -and $Script:DateTimeAsISO8601) {
Write-Verbose -Message "Got a DateTime and will format it as ISO 8601."
"""$($InputObject.ToString('yyyy\-MM\-ddTHH\:mm\:ss'))"""
}
elseif ($InputObject -is [HashTable]) {
$Keys = @($InputObject.Keys)
Write-Verbose -Message "Input object is a hash table (keys: $($Keys -join ', '))."
}
elseif ($InputObject.GetType().FullName -eq "System.Management.Automation.PSCustomObject") {
$Keys = @(Get-Member -InputObject $InputObject -MemberType NoteProperty |
Select-Object -ExpandProperty Name)
Write-Verbose -Message "Input object is a custom PowerShell object (properties: $($Keys -join ', '))."
}
elseif ($InputObject.GetType().Name -match '\[\]|Array') {
Write-Verbose -Message "Input object appears to be of a collection/array type. Building JSON for array input object."
$Json += "[`n" + (($InputObject | ForEach-Object {
if ($null -eq $_) {
Write-Verbose -Message "Got null inside array."
" " * ((4 * ($WhiteSpacePad / 4)) + 4) + "null"
}
elseif ($_ -is [Bool] -and $_ -eq $true) {
Write-Verbose -Message "Got 'true' inside array."
" " * ((4 * ($WhiteSpacePad / 4)) + 4) + "true"
}
elseif ($_ -is [Bool] -and $_ -eq $false) {
Write-Verbose -Message "Got 'false' inside array."
" " * ((4 * ($WhiteSpacePad / 4)) + 4) + "false"
}
elseif ($_ -is [DateTime] -and $Script:DateTimeAsISO8601) {
Write-Verbose -Message "Got a DateTime and will format it as ISO 8601."
" " * ((4 * ($WhiteSpacePad / 4)) + 4) + """$($_.ToString('yyyy\-MM\-ddTHH\:mm\:ss'))"""
}
elseif ($_ -is [HashTable] -or $_.GetType().FullName -eq "System.Management.Automation.PSCustomObject" -or $_.GetType().Name -match '\[\]|Array') {
Write-Verbose -Message "Found array, hash table or custom PowerShell object inside array."
" " * ((4 * ($WhiteSpacePad / 4)) + 4) + (ConvertToJsonInternal -InputObject $_ -WhiteSpacePad ($WhiteSpacePad + 4)) -replace '\s*,\s*$'
}
else {
Write-Verbose -Message "Got a number or string inside array."
$TempJsonString = GetNumberOrString -InputObject $_
" " * ((4 * ($WhiteSpacePad / 4)) + 4) + $TempJsonString
}
}) -join ",`n") + "`n$(" " * (4 * ($WhiteSpacePad / 4)))],`n"
}
else {
Write-Verbose -Message "Input object is a single element (treated as string/number)."
GetNumberOrString -InputObject $InputObject
}
if ($Keys.Count) {
Write-Verbose -Message "Building JSON for hash table or custom PowerShell object."
$Json += "{`n"
foreach ($Key in $Keys) {
# -is [PSCustomObject]) { # this was buggy with calculated properties, the value was thought to be PSCustomObject
if ($null -eq $InputObject.$Key) {
Write-Verbose -Message "Got null as `$InputObject.`$Key in inner hash or PS object."
$Json += " " * ((4 * ($WhiteSpacePad / 4)) + 4) + """$Key"": null,`n"
}
elseif ($InputObject.$Key -is [Bool] -and $InputObject.$Key -eq $true) {
Write-Verbose -Message "Got 'true' in `$InputObject.`$Key in inner hash or PS object."
$Json += " " * ((4 * ($WhiteSpacePad / 4)) + 4) + """$Key"": true,`n" }
elseif ($InputObject.$Key -is [Bool] -and $InputObject.$Key -eq $false) {
Write-Verbose -Message "Got 'false' in `$InputObject.`$Key in inner hash or PS object."
$Json += " " * ((4 * ($WhiteSpacePad / 4)) + 4) + """$Key"": false,`n"
}
elseif ($InputObject.$Key -is [DateTime] -and $Script:DateTimeAsISO8601) {
Write-Verbose -Message "Got a DateTime and will format it as ISO 8601."
$Json += " " * ((4 * ($WhiteSpacePad / 4)) + 4) + """$Key"": ""$($InputObject.$Key.ToString('yyyy\-MM\-ddTHH\:mm\:ss'))"",`n"
}
elseif ($InputObject.$Key -is [HashTable] -or $InputObject.$Key.GetType().FullName -eq "System.Management.Automation.PSCustomObject") {
Write-Verbose -Message "Input object's value for key '$Key' is a hash table or custom PowerShell object."
$Json += " " * ($WhiteSpacePad + 4) + """$Key"":`n$(" " * ($WhiteSpacePad + 4))"
$Json += ConvertToJsonInternal -InputObject $InputObject.$Key -WhiteSpacePad ($WhiteSpacePad + 4)
}
elseif ($InputObject.$Key.GetType().Name -match '\[\]|Array') {
Write-Verbose -Message "Input object's value for key '$Key' has a type that appears to be a collection/array."
Write-Verbose -Message "Building JSON for ${Key}'s array value."
$Json += " " * ($WhiteSpacePad + 4) + """$Key"":`n$(" " * ((4 * ($WhiteSpacePad / 4)) + 4))[`n" + (($InputObject.$Key | ForEach-Object {
if ($null -eq $_) {
Write-Verbose -Message "Got null inside array inside inside array."
" " * ((4 * ($WhiteSpacePad / 4)) + 8) + "null"
}
elseif ($_ -is [Bool] -and $_ -eq $true) {
Write-Verbose -Message "Got 'true' inside array inside inside array."
" " * ((4 * ($WhiteSpacePad / 4)) + 8) + "true"
}
elseif ($_ -is [Bool] -and $_ -eq $false) {
Write-Verbose -Message "Got 'false' inside array inside inside array."
" " * ((4 * ($WhiteSpacePad / 4)) + 8) + "false"
}
elseif ($_ -is [DateTime] -and $Script:DateTimeAsISO8601) {
Write-Verbose -Message "Got a DateTime and will format it as ISO 8601."
" " * ((4 * ($WhiteSpacePad / 4)) + 8) + """$($_.ToString('yyyy\-MM\-ddTHH\:mm\:ss'))"""
}
elseif ($_ -is [HashTable] -or $_.GetType().FullName -eq "System.Management.Automation.PSCustomObject" `
-or $_.GetType().Name -match '\[\]|Array') {
Write-Verbose -Message "Found array, hash table or custom PowerShell object inside inside array."
" " * ((4 * ($WhiteSpacePad / 4)) + 8) + (ConvertToJsonInternal -InputObject $_ -WhiteSpacePad ($WhiteSpacePad + 8)) -replace '\s*,\s*$'
}
else {
Write-Verbose -Message "Got a string or number inside inside array."
$TempJsonString = GetNumberOrString -InputObject $_
" " * ((4 * ($WhiteSpacePad / 4)) + 8) + $TempJsonString
}
}) -join ",`n") + "`n$(" " * (4 * ($WhiteSpacePad / 4) + 4 ))],`n"
}
else {
Write-Verbose -Message "Got a string inside inside hashtable or PSObject."
# '\\(?!["/bfnrt]|u[0-9a-f]{4})'
$TempJsonString = GetNumberOrString -InputObject $InputObject.$Key
$Json += " " * ((4 * ($WhiteSpacePad / 4)) + 4) + """$Key"": $TempJsonString,`n"
}
}
$Json = $Json -replace '\s*,$' # remove trailing comma that'll break syntax
$Json += "`n" + " " * $WhiteSpacePad + "},`n"
}
$Json
}
function ConvertTo-STJson {
[CmdletBinding()]
#[OutputType([Void], [Bool], [String])]
Param(
[AllowNull()]
[Parameter(Mandatory=$True,
ValueFromPipeline=$True,
ValueFromPipelineByPropertyName=$True)]
$InputObject,
[Switch] $Compress,
[Switch] $CoerceNumberStrings = $False,
[Switch] $DateTimeAsISO8601 = $False)
Begin{
$JsonOutput = ""
$Collection = @()
# Not optimal, but the easiest now.
[Bool] $Script:CoerceNumberStrings = $CoerceNumberStrings
[Bool] $Script:DateTimeAsISO8601 = $DateTimeAsISO8601
[String] $Script:NumberRegex = '^-?\d+(?:(?:\.\d+)?(?:e[+\-]?\d+)?)?$'
#$Script:NumberAndValueRegex = '^-?\d+(?:(?:\.\d+)?(?:e[+\-]?\d+)?)?$|^(?:true|false|null)$'
}
Process {
# Hacking on pipeline support ...
if ($_) {
Write-Verbose -Message "Adding object to `$Collection. Type of object: $($_.GetType().FullName)."
$Collection += $_
}
}
End {
if ($Collection.Count) {
Write-Verbose -Message "Collection count: $($Collection.Count), type of first object: $($Collection[0].GetType().FullName)."
$JsonOutput = ConvertToJsonInternal -InputObject ($Collection | ForEach-Object { $_ })
}
else {
$JsonOutput = ConvertToJsonInternal -InputObject $InputObject
}
if ($null -eq $JsonOutput) {
Write-Verbose -Message "Returning `$null."
return $null # becomes an empty string :/
}
elseif ($JsonOutput -is [Bool] -and $JsonOutput -eq $true) {
Write-Verbose -Message "Returning `$true."
[Bool] $true # doesn't preserve bool type :/ but works for comparisons against $true
}
elseif ($JsonOutput-is [Bool] -and $JsonOutput -eq $false) {
Write-Verbose -Message "Returning `$false."
[Bool] $false # doesn't preserve bool type :/ but works for comparisons against $false
}
elseif ($Compress) {
Write-Verbose -Message "Compress specified."
(
($JsonOutput -split "\n" | Where-Object { $_ -match '\S' }) -join "`n" `
-replace '^\s*|\s*,\s*$' -replace '\ *\]\ *$', ']'
) -replace ( # these next lines compress ...
'(?m)^\s*("(?:\\"|[^"])+"): ((?:"(?:\\"|[^"])+")|(?:null|true|false|(?:' + `
$Script:NumberRegex.Trim('^$') + `
')))\s*(?<Comma>,)?\s*$'), "`${1}:`${2}`${Comma}`n" `
-replace '(?m)^\s*|\s*\z|[\r\n]+'
}
else {
($JsonOutput -split "\n" | Where-Object { $_ -match '\S' }) -join "`n" `
-replace '^\s*|\s*,\s*$' -replace '\ *\]\ *$', ']'
}
}
}