Skip to content

feat: Map qualified, nullable and class-typed properties to correct JSON schema types - #5

Open
Gijsreyn wants to merge 3 commits into
mainfrom
feature/type-mapping-and-defs
Open

feat: Map qualified, nullable and class-typed properties to correct JSON schema types#5
Gijsreyn wants to merge 3 commits into
mainfrom
feature/type-mapping-and-defs

Conversation

@Gijsreyn

@Gijsreyn Gijsreyn commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

Adapted resource manifests generated from class-based resources that declare their property types the fully qualified way ([System.Boolean], [System.Int32], [System.Nullable[T]]) silently fell back to "string", because the type map only knew the short aliases. Properties typed as a class from the same file and PSCredential were mapped to strings as well, and a [ValidateSet()] on an array or numeric property produced a top-level string enum. Consumers such as Microsoft365DSC and AzureDevOpsDsc worked around all of this with post-processing scripts.

This change makes the tool produce the correct schema on its own.

Example

Given this resource:

class Segment
{
    [DscProperty(Key)]
    [System.ComponentModel.Description('The segment name.')]
    [System.String] $Name

    [DscProperty()]
    [System.ComponentModel.Description('The allowed origins.')]
    [System.String[]] $AllowedOrigins
}

[DscResource()]
class MyResource
{
    [DscProperty(Key)]
    [System.ComponentModel.Description('The unique name of the resource.')]
    [System.String] $Name

    [DscProperty()]
    [System.ComponentModel.Description('Whether the resource is enabled.')]
    [System.Boolean] $Enabled

    [DscProperty()]
    [System.ComponentModel.Description('The scan level.')]
    [ValidateSet('0', '1', '2')]
    [System.Nullable[System.Int32]] $Level

    [DscProperty()]
    [System.ComponentModel.Description('The allowed protocols.')]
    [ValidateSet('Http', 'Https')]
    [System.String[]] $Protocols

    [DscProperty()]
    [System.ComponentModel.Description('The credential used to connect.')]
    [System.Management.Automation.PSCredential] $Credential

    [DscProperty()]
    [System.ComponentModel.Description('All segments.')]
    [Segment[]] $Segments

    [MyResource] Get() { return $this }
    [System.Boolean] Test() { return $true }
    [void] Set() { }
    [System.String] Export() { return '' }
}

the embedded schema before this change was:

{
  "properties": {
    "Name":       { "type": "string", "title": "Name",       "description": "The Name property." },
    "Enabled":    { "type": "string", "title": "Enabled",    "description": "The Enabled property." },
    "Level":      { "type": "string", "enum": ["0", "1", "2"], "title": "Level", "description": "The Level property." },
    "Protocols":  { "type": "string", "enum": ["Http", "Https"], "title": "Protocols", "description": "The Protocols property." },
    "Credential": { "type": "string", "title": "Credential", "description": "The Credential property." },
    "Segments":   { "type": "array", "items": { "type": "string" }, "title": "Segments", "description": "The Segments property." }
  }
}

with "capabilities": ["get", "set", "test", "export"], and after it is:

{
  "properties": {
    "Name":       { "type": "string",  "title": "Name",    "description": "The unique name of the resource." },
    "Enabled":    { "type": "boolean", "title": "Enabled", "description": "Whether the resource is enabled." },
    "Level":      { "type": "integer", "enum": [0, 1, 2], "title": "Level", "description": "The scan level." },
    "Protocols":  { "type": "array", "items": { "type": "string", "enum": ["Http", "Https"] }, "title": "Protocols", "description": "The allowed protocols." },
    "Credential": { "$ref": "#/$defs/PSCredential", "title": "Credential", "description": "The credential used to connect." },
    "Segments":   { "type": "array", "items": { "$ref": "#/$defs/Segment" }, "title": "Segments", "description": "All segments." }
  },
  "$defs": {
    "PSCredential": {
      "type": "object",
      "properties": {
        "username": { "type": "string" },
        "password": { "type": "string" }
      }
    },
    "Segment": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "Name":           { "type": "string", "title": "Name", "description": "The segment name." },
        "AllowedOrigins": { "type": "array", "items": { "type": "string" }, "title": "AllowedOrigins", "description": "The allowed origins." }
      }
    }
  }
}

with "capabilities": ["get", "set", "test"], because the adapter invokes Export on the type and an instance Export() can never run.

Generating from source files

A module that keeps one source file per resource can now name the manifests after the built module without post-processing:

Get-ChildItem -Path ./source/Resources -Filter *.psm1 -Recurse |
    New-DscAdaptedResourceManifest -ModuleManifestPath ./output/MyModule/MyModule.psd1

Every manifest carries "type": "MyModule/<Class>", "path": "MyModule.psd1" and the version and author of MyModule.psd1. A four-part ModuleVersion such as 1.26.1007.1 is written as 1.26.1007, because the manifest requires a semantic version.

Changes

  • Fully qualified names, Nullable[T], generic collections and additional CLR types (Guid, TimeSpan, SecureString, unsigned integers, ...) map to their JSON schema types.
  • Classes defined in the same file are emitted once under $defs and referenced with $ref, including arrays of a class and nested classes. A class without [DscProperty()] members is described by its public instance properties.
  • PSCredential references a shared definition with username and password, the shape the PowerShell adapter turns into a credential.
  • [ValidateSet()] values go under items for arrays and are converted to the mapped type for numeric and boolean properties.
  • Property descriptions fall back to [System.ComponentModel.Description()] attributes when the comment-based help has no entry.
  • New -ModuleManifestPath on New-DscAdaptedResourceManifest; four-part versions are truncated to Major.Minor.Build.
  • export is only advertised for a static Export().
  • The default $schema is the canonical https://aka.ms/dsc/schemas/v3/bundled/resource/adapted/manifest.json; DSC 3.3 reports the old URI as deprecated.
  • The build tasks write UTF-8 without a BOM, and ConvertTo-DscPropertyOverrideFromConfig is public so the tasks can apply PropertyOverrides from build.yaml.
  • The Windows PowerShell test job is fixed: Pester 6's profiler-based coverage fails with Index was out of range on class instantiation under 5.1, so build.yaml sets CodeCoverage.UseBreakpoints: true. main fails the same way today.

Verification

  • 588 unit tests, 94% coverage, on pwsh and Windows PowerShell 5.1.
  • Microsoft365DSC: 531 resources regenerated; dsc resource list and dsc resource schema on dsc 3.3.0-preview.4 return them with their $defs and no deprecation warning.
  • AzureDevOpsDsc: 49 resources; its V3 manifest test suite passes without its type-fixing build task.
  • SqlServerDsc: 6 resources with correct boolean, integer, $defs/SqlReason, $defs/DatabasePermission and $defs/PSCredential.

This change is Reviewable

@coderabbitai

coderabbitai Bot commented Sep 6, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 06270b04-3b44-4ef4-bde1-6202e36496ca


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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