← BACK TO HOME

Building DDM Declarations, Part 2: PowerShell Object Manipulation for DDM

Shows how to build DDM Configuration and Activation declarations with PowerShell objects, arrays, nested hashtables, predicates, and ConvertTo-Json -Depth 10.

Part 1 established the basic DDM declaration shape: Type, Identifier, ServerToken, and Payload.

That schema is simple enough to read by eye, but it should not be built as raw text. The moment a declaration contains arrays, nested objects, booleans, integers, predicates, or multiple linked declarations, string manipulation becomes a liability.

This is where PowerShell is a strong fit for Apple administration. PowerShell can build structured data as structured data. A hashtable becomes a JSON object. An array becomes a JSON array. $true becomes true. An integer remains an integer. There is no need to manually count braces, escape quotation marks, or splice strings together with sed, awk, or regular expressions.

This article focuses exclusively on using PowerShell objects to construct DDM JSON.

Why String Manipulation Fails

A DDM declaration is not a text template. It is a structured object.

Consider a simple activation declaration:

{
  "Type": "com.apple.activation.simple",
  "Identifier": "com.franca.tech.activation.security-baseline.v1",
  "ServerToken": "2026-05-28T16:05:00Z",
  "Payload": {
    "StandardConfigurations": [
      "com.franca.tech.config.passcode-baseline.v1"
    ],
    "Predicate": "(@status(device.operating-system.family) == 'macOS')"
  }
}

A string-based approach has to preserve every comma, brace, bracket, quote, and data type. It also has to safely insert values into nested positions. That may be tolerable for one static declaration. It fails quickly when the configuration identifier is dynamic, when multiple configurations are added to an activation, or when predicates are generated from environment-specific input.

PowerShell can represent that same declaration as an object graph. Once the object graph is correct, ConvertTo-Json performs the serialization.

That is the administrative pattern Windows engineers already rely on. You do not parse Get-Process output with awk when PowerShell gives you process objects. You do not scrape registry text when PowerShell gives you registry provider objects. DDM JSON should be handled the same way.

Building Identifiers First

Start by defining stable identifiers. The configuration declaration needs an identifier, and the activation declaration needs to reference that identifier.

$ConfigurationIdentifier = "com.franca.tech.config.passcode-baseline.v1"
$ActivationIdentifier    = "com.franca.tech.activation.security-baseline.v1"

The configuration identifier represents the actual setting declaration. The activation identifier represents the declaration that activates the configuration.

These values should be predictable and durable. They should not be replaced with random GUIDs on every run unless the intent is to create entirely new declarations every time. In most environments, the identifier remains stable while the ServerToken changes when the payload changes.

Apple’s declaration schema says Identifier should not exceed 64 octets, so build naming conventions with that limit in mind. Reverse-DNS style naming is still useful, but avoid turning the identifier into a full sentence.

Building the Configuration Declaration

The following configuration declaration defines a simple passcode baseline.

$ConfigurationPayload = [ordered]@{
    RequirePasscode             = $true
    MinimumLength               = 8
    RequireComplexPasscode      = $true
    MaximumFailedAttempts       = 10
    MaximumGracePeriodInMinutes = 5
}

$ConfigurationDeclaration = [ordered]@{
    Type        = "com.apple.configuration.passcode.settings"
    Identifier  = $ConfigurationIdentifier
    ServerToken = "passcode-baseline-2026-05-28-v1"
    Payload     = $ConfigurationPayload
}

The key point is that the payload is not a string. It is an ordered hashtable. The booleans are real booleans. The integers are real integers. The nested object is a real nested object.

That matters because the JSON serializer can preserve the intended data types:

"RequirePasscode": true

not:

"RequirePasscode": "true"

Those are not equivalent. One is a Boolean value. The other is a string.

The ServerToken is also intentionally short. Apple’s declaration schema says ServerToken should not exceed 64 octets. Treat it as an opaque version marker, not as a place to store a long change description.

Building the Activation Declaration

An activation declaration links one or more configuration declarations by identifier.

The StandardConfigurations key is an array. Even when only one configuration is referenced, it should still be represented as an array.

$StandardConfigurationIdentifiers = [System.Collections.Generic.List[string]]::new()
[void]$StandardConfigurationIdentifiers.Add($ConfigurationIdentifier)

$ActivationPayload = [ordered]@{
    StandardConfigurations = @($StandardConfigurationIdentifiers)
    Predicate              = "(@status(device.operating-system.family) == 'macOS')"
}

$ActivationDeclaration = [ordered]@{
    Type        = "com.apple.activation.simple"
    Identifier  = $ActivationIdentifier
    ServerToken = "activation-security-baseline-2026-05-28-v1"
    Payload     = $ActivationPayload
}

The activation does not embed the configuration declaration. It references the configuration declaration by identifier.

The Predicate is inside the activation Payload because it is part of the com.apple.activation.simple payload schema. The predicate is optional. When present, the activation only applies when the predicate evaluates to true. When absent, the activation is not gated by that additional predicate condition.

That relationship is the part most likely to be built incorrectly when using string manipulation. With PowerShell objects, it is explicit: the activation payload contains an array of strings, and each string is the identifier of a configuration declaration.

Assembling a Local Declaration Bundle

Apple DDM declarations are individual objects, but during development it is useful to hold related declarations together in a local bundle.

$DeclarationBundle = [ordered]@{
    Declarations = @(
        $ConfigurationDeclaration
        $ActivationDeclaration
    )
}

This wrapper is useful for local review, testing, source control, and pipeline handling. Whether the final MDM workflow expects a single declaration, separate declaration objects, or a vendor-specific wrapper depends on the MDM implementation. The important point is that the declarations inside the bundle remain valid DDM declaration objects.

Complete Script

The full construction script looks like this:

# PowerShell 7+
# Build DDM Configuration and Activation declarations as native objects.

$ConfigurationIdentifier = "com.franca.tech.config.passcode-baseline.v1"
$ActivationIdentifier    = "com.franca.tech.activation.security-baseline.v1"

# Configuration declaration payload.
# These values are typed as Boolean and Integer values, not strings.
$ConfigurationPayload = [ordered]@{
    RequirePasscode             = $true
    MinimumLength               = 8
    RequireComplexPasscode      = $true
    MaximumFailedAttempts       = 10
    MaximumGracePeriodInMinutes = 5
}

# Configuration declaration.
$ConfigurationDeclaration = [ordered]@{
    Type        = "com.apple.configuration.passcode.settings"
    Identifier  = $ConfigurationIdentifier
    ServerToken = "passcode-baseline-2026-05-28-v1"
    Payload     = $ConfigurationPayload
}

# Activation payload.
# StandardConfigurations is an array of configuration identifiers.
# Predicate is optional and is evaluated by the client as activation logic.
$StandardConfigurationIdentifiers = [System.Collections.Generic.List[string]]::new()
[void]$StandardConfigurationIdentifiers.Add($ConfigurationIdentifier)

$ActivationPayload = [ordered]@{
    StandardConfigurations = @($StandardConfigurationIdentifiers)
    Predicate              = "(@status(device.operating-system.family) == 'macOS')"
}

# Activation declaration.
$ActivationDeclaration = [ordered]@{
    Type        = "com.apple.activation.simple"
    Identifier  = $ActivationIdentifier
    ServerToken = "activation-security-baseline-2026-05-28-v1"
    Payload     = $ActivationPayload
}

# Local development bundle.
# This is useful for review and source control.
$DeclarationBundle = [ordered]@{
    Declarations = @(
        $ConfigurationDeclaration
        $ActivationDeclaration
    )
}

# Review the JSON shape before submission or source control commit.
$DeclarationBundle | ConvertTo-Json -Depth 10

This is easier to review than a hand-built JSON string because every relationship is visible in the object model.

The configuration declaration has a payload. The activation declaration has a payload. The activation payload contains an array. The array contains the configuration identifier. The predicate is in the activation payload.

Manipulating the Object Before Serialization

Because the declaration is an object, it can be modified safely before converting to JSON.

For example, increasing the minimum passcode length is a normal property assignment:

$ConfigurationDeclaration.Payload.MinimumLength = 12
$ConfigurationDeclaration.ServerToken = "passcode-baseline-2026-05-28-v2"

Adding another configuration to the activation is an array operation:

$AdditionalConfigurationIdentifier = "com.franca.tech.config.security-extra.v1"
[void]$StandardConfigurationIdentifiers.Add($AdditionalConfigurationIdentifier)
$ActivationDeclaration.ServerToken = "activation-security-baseline-2026-05-28-v2"

The [void] cast suppresses the return value from .Add(), which keeps the console and pipeline clean. Without it, the method returns the new count of the list, which is usually not useful output in a script.

The important operational rule is that a content change should be paired with a ServerToken change. The identifier tells the client which declaration this is. The server token tells the client whether this version is new.

Serializing with ConvertTo-Json

After the object graph is correct, serialize it.

$DdmJson = $DeclarationBundle | ConvertTo-Json -Depth 10
$DdmJson

For API submission or compact storage, add -Compress:

$DdmJsonCompressed = $DeclarationBundle | ConvertTo-Json -Depth 10 -Compress
$DdmJsonCompressed

The -Depth parameter is not optional for serious DDM work. The default depth for ConvertTo-Json is too shallow for nested management payloads. A depth of 10 is a safe default for this type of declaration construction.

Without an adequate depth value, PowerShell can serialize nested objects incorrectly, producing output that looks superficially valid but does not contain the full object structure. That is exactly the type of failure that leads to confusing MDM-side validation errors.

Validating the JSON Round Trip

During development, perform a local round-trip test. Convert the object to JSON, then parse it back into an object.

$RoundTrip = $DdmJson | ConvertFrom-Json -Depth 10

$RoundTrip.Declarations[0].Type
$RoundTrip.Declarations[0].Payload.MinimumLength
$RoundTrip.Declarations[1].Payload.StandardConfigurations
$RoundTrip.Declarations[1].Payload.Predicate

On PowerShell 7, ConvertFrom-Json already handles deep JSON much more safely than the shallow default behavior that causes problems with ConvertTo-Json. Keeping -Depth 10 in the round-trip example is still useful because it makes the serialization and deserialization expectations explicit and keeps the code aligned with the article’s PowerShell 7 target.

This does not prove that the declaration is accepted by every MDM platform, but it does prove that the JSON is syntactically valid and that the major object relationships survived serialization.

That catches a different class of problem than vendor-side validation. Local JSON validation confirms that the PowerShell object became valid JSON. MDM validation confirms that the JSON matches the declaration schema and vendor workflow.

The Practical Rule

Do not build DDM JSON with string concatenation.

Build the declaration as a PowerShell object. Use ordered hashtables when predictable output helps review. Use arrays for JSON arrays. Use real booleans and integers. Put activation predicates inside the activation payload. Update the ServerToken when the declaration content changes. Convert with ConvertTo-Json -Depth 10.

That workflow is repeatable, reviewable, and automation-friendly.

Part 3 moves from payload construction to Jamf Pro. The important distinction is that the JSON object is only one part of the deployment process. The next step is understanding which Jamf Pro workflows and API endpoints are supported for custom DDM declarations, which endpoints are for verification, and which invented shortcuts should not be scripted.