Functions/GenXdev.Helpers/Get-JsonExampleFromSchema.ps1

<###############################################################################
.SYNOPSIS
Generates a human-readable example JSON string from a JSON Schema definition.

.DESCRIPTION
Converts any JSON Schema (OpenAI json_schema format or raw schema) into a
compact example JSON string suitable for injecting into LLM system
instructions. Handles nested objects, arrays, scalar types, enums, unions,
oneOf/anyOf/allOf compositions, and format hints like date-time and email.

The function builds a PowerShell object tree by recursively walking the
schema, then serializes it to a compact JSON string via ConvertTo-Json.

.LICENSE
Copyright (C) 2026 René Vaessen / GenXdev

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.

.PARAMETER Schema
A JSON Schema definition. Accepts either a JSON string or a pre-parsed
PSCustomObject. Supports both the OpenAI json_schema wrapper format
({ json_schema: { schema: ... } }) and raw JSON Schema objects.

.EXAMPLE
$schema = '{ "type": "object", "properties": { "name": { "type": "string" } } }'
Get-JsonExampleFromSchema -Schema $schema

Generates: {"name":"..."}

.EXAMPLE
$openAiSchema = '{ "type": "json_schema", "json_schema": { "name": "response",
"strict": true, "schema": { "type": "object", "properties": {
"items": { "type": "array", "items": { "type": "object", "properties": {
"id": { "type": "integer" }, "label": { "type": "string" } } } } } } } }'
Get-JsonExampleFromSchema -Schema $openAiSchema

Generates: {"items":[{"id":0,"label":"..."}]}
#>

###############################################################################
function Get-JsonExampleFromSchema {

    [CmdletBinding()]
    [OutputType([string])]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseDeclaredVarsMoreThanAssignments', '')]
    param(
        #######################################################################
        [Parameter(
            Mandatory = $false,
            Position = 0,
            HelpMessage = 'A JSON Schema definition (JSON string or ' +
                'PSCustomObject). Supports OpenAI json_schema wrapper ' +
                'format and raw JSON Schema objects.'
        )]
        [object] $Schema
        #######################################################################
    )

    begin {
        Microsoft.PowerShell.Utility\Write-Verbose (
            'Starting JSON Schema to example conversion.')

        # Recursively build an example PowerShell value from a schema node.
        # Returns a PS value: hashtable (object), array, string, int, bool,
        # or $null.
        function BuildExample($node, [int] $depth = 0) {

            # Guard against runaway recursion
            if ($depth -gt 10) {
                return '...'
            }

            # Unwrap PSObject to its base object.
            # Guard: PSCustomObject inherits from PSObject and has
            # no meaningful BaseObject ($null), so only unwrap when
            # BaseObject is a concrete type.
            if ($node -is [System.Management.Automation.PSObject] -and
                $null -ne $node.BaseObject) {
                $node = $node.BaseObject
            }

            # Handle null / empty input
            if ($null -eq $node) {
                return @{ response = '...' }
            }

            # If it's a string, try to parse as JSON
            if ($node -is [string]) {
                $trimmed = $node.Trim()
                if ([string]::IsNullOrWhiteSpace($trimmed)) {
                    return '...'
                }
                try {
                    $node = $trimmed |
                        Microsoft.PowerShell.Utility\ConvertFrom-Json
                }
                catch {
                    return '...'
                }
            }

            # Auto-detect OpenAI json_schema wrapper and unwrap it
            if ($node.json_schema -and $node.json_schema.schema) {
                $node = $node.json_schema.schema
            }

            # ── Type Dispatch ──────────────────────────────────────────

            # 1. enum — use the first enum value
            if ($node.enum) {
                $enums = @($node.enum)
                if ($enums.Count -gt 0) {
                    return $enums[0]
                }
            }

            # 2. const — use the const value directly
            if ($null -ne $node.const) {
                return $node.const
            }

            # 3. oneOf / anyOf — use the first schema option
            if ($node.oneOf) {
                $options = @($node.oneOf)
                if ($options.Count -gt 0) {
                    return BuildExample $options[0] ($depth + 1)
                }
            }
            if ($node.anyOf) {
                $options = @($node.anyOf)
                if ($options.Count -gt 0) {
                    return BuildExample $options[0] ($depth + 1)
                }
            }

            # 4. allOf — merge all sub-schemas into a single object
            if ($node.allOf) {
                $merged = [ordered]@{}
                foreach ($sub in @($node.allOf)) {
                    $subResult = BuildExample $sub ($depth + 1)
                    if ($subResult -is [hashtable]) {
                        foreach ($key in $subResult.Keys) {
                            $merged[$key] = $subResult[$key]
                        }
                    }
                }
                if ($merged.Count -gt 0) {
                    return $merged
                }
                # Fall through if allOf produced nothing
            }

            # 5. Resolve the effective type, handling union type arrays
            # (e.g. ["string", "null"])
            $effectiveType = $node.type
            if ($effectiveType -is [array] -or
                $effectiveType -is [object[]]) {

                # Pick the first non-null type
                foreach ($t in $effectiveType) {
                    if ($t -ne 'null') {
                        $effectiveType = "$t"
                        break
                    }
                }
                # If all were null, keep it as null
                if ($effectiveType -is [array]) {
                    $effectiveType = 'null'
                }
            }

            # 6. object type (has properties or explicit type).
            # Use a helper to safely detect a real schema properties
            # collection, avoiding false positives from PSObject intrinsic
            # members when the node is a wrapped PSCustomObject.
            $hasRealProperties = _HasSchemaProperties $node
            if ($effectiveType -eq 'object' -or $hasRealProperties) {

                $result = [ordered]@{}
                if ($node.properties) {
                    $props = $node.properties
                    if ($props -is [System.Management.Automation.PSObject]) {
                        $props = $props.PSObject.Properties
                    }
                    if ($props -is [System.Collections.IDictionary]) {
                        foreach ($key in $props.Keys) {
                            $result["$key"] = BuildExample $props[$key] ($depth + 1)
                        }
                    }
                    else {
                        foreach ($prop in $props) {
                            $propName = if ($prop -is
                                [System.Management.Automation.PSPropertyInfo]) {
                                $prop.Name
                            }
                            else {
                                $prop.Key
                            }
                            $propValue = if ($prop -is
                                [System.Management.Automation.PSPropertyInfo]) {
                                $prop.Value
                            }
                            else {
                                $prop.Value
                            }
                            $result["$propName"] = BuildExample $propValue ($depth + 1)
                        }
                    }
                }
                return $result
            }

            # 7. array type
            if ($effectiveType -eq 'array') {
                # Comma prefix prevents PowerShell from unrolling the array
                # during function output. Without it, return @($x) unrolls
                # and the caller gets a single element, not an array.
                if ($node.items) {
                    return , @(BuildExample $node.items ($depth + 1))
                }
                return , @()
            }

            # 8. string type (with optional format hints)
            if ($effectiveType -eq 'string') {
                if ($node.format) {
                    switch -Regex ($node.format) {
                        '^date-time$' {
                            return '2024-01-15T10:30:00Z'
                        }
                        '^date$' {
                            return '2024-01-15'
                        }
                        '^email$' {
                            return 'user@example.com'
                        }
                        '^uri$|^url$' {
                            return 'https://example.com'
                        }
                        '^uuid$' {
                            return '550e8400-e29b-41d4-a716-446655440000'
                        }
                        '^ipv4$' {
                            return '192.168.1.1'
                        }
                        '^ipv6$' {
                            return '::1'
                        }
                        '^hostname$' {
                            return 'example.com'
                        }
                        '^json-pointer$' {
                            return '/path/to/property'
                        }
                        '^regex$' {
                            return '^[a-z]+$'
                        }
                        default {
                            if ($node.description) {
                                return "$($node.description)"
                            }
                            return '...'
                        }
                    }
                }
                if ($node.description) {
                    return "$($node.description)"
                }
                return '...'
            }

            # 9. number / integer
            if ($effectiveType -eq 'number' -or
                $effectiveType -eq 'integer') {

                if ($node.description) {
                    return "$($node.description)"
                }
                return 0
            }

            # 10. boolean
            if ($effectiveType -eq 'boolean') {
                return $true
            }

            # 11. null
            if ($effectiveType -eq 'null') {
                return $null
            }

            # 12. Fallback — unknown or missing type, return a default object
            if ($node.description) {
                return "$($node.description)"
            }
            return @{ response = '...' }
        }

        # Helper: returns $true when $node has a real schema properties
        # collection (a Hashtable with keys), avoiding false positives
        # from PSObject intrinsic ".Properties" member on PSCustomObjects.
        function _HasSchemaProperties($node) {
            if ($null -eq $node) { return $false }
            if ($node -is [System.Management.Automation.PSObject]) {
                $node = $node.BaseObject
            }
            # Only dictionary types can have .properties as a key lookup
            if ($node -isnot [System.Collections.IDictionary]) {
                return $false
            }
            $props = $node['properties']
            return ($props -is [System.Collections.IDictionary]) -and
                ($props.Keys.Count -gt 0)
        }

        # Serializes a schema node to JSON string and writes it to output.
        function ConvertAndEmit($schemaNode) {
            $exampleValue = BuildExample $schemaNode 0

            if ($exampleValue -is [string]) {
                $json = '"' + (
                    $exampleValue -replace '\\', '\\\\' -replace '"', '\\"'
                ) + '"'
            }
            elseif ($null -eq $exampleValue) {
                $json = 'null'
            }
            elseif ($exampleValue -is [bool]) {
                $json = if ($exampleValue) { 'true' } else { 'false' }
            }
            elseif ($exampleValue -is [int] -or
                $exampleValue -is [long] -or
                $exampleValue -is [double]) {
                $json = [string] $exampleValue
            }
            else {
                # Hashtable, array, or PSCustomObject — use ConvertTo-Json
                # with -InputObject to preserve array structure.
                $json = Microsoft.PowerShell.Utility\ConvertTo-Json `
                    -InputObject $exampleValue `
                    -Depth 20 -Compress `
                    -WarningAction SilentlyContinue `
                    -ErrorAction SilentlyContinue

                if ([string]::IsNullOrWhiteSpace($json)) {
                    $json = '{"response":"..."}'
                }
            }

            Microsoft.PowerShell.Utility\Write-Verbose (
                "Generated schema example: $json")
            return $json
        }
    }

    end {
        if ($null -eq $Schema) {
            return '{"response":"..."}'
        }

        # Normalize: unwrap PSObject wrapping
        $schemaNode = $Schema
        if ($schemaNode -is [System.Management.Automation.PSObject]) {
            $base = $schemaNode.BaseObject
            if ($base -is [System.Collections.IDictionary]) {
                $schemaNode = $base
            }
        }

        try {
            ConvertAndEmit $schemaNode
        }
        catch {
            Microsoft.PowerShell.Utility\Write-Verbose (
                "Schema example generation failed: $($_.Exception.Message)")
            '{"response":"..."}'
        }
    }
}
###############################################################################