Skip to content

Invoke-MgGraphRequest -Body serialization fails on PSObject-wrapped pipeline strings #3654

Description

@Virgil-Bulens

Describe the bug

Invoke-MgGraphRequest -Body serializes IDictionary bodies with Newtonsoft.Json. If a hashtable body contains a value that came from a PowerShell pipeline (for example, bare $_ from ForEach-Object) Newtonsoft sees a System.Management.Automation.PSObject wrapper rather than a plain CLR string and reflects into PowerShell adapted members on the string.

For a string value, this exposes the Chars indexed property as a PSParameterizedProperty. Serialization then fails with a circular/self-reference error before the HTTP request can be sent.

Observed exception:

Newtonsoft.Json.JsonSerializationException: Self referencing loop detected for property 'Value' with type 'System.Management.Automation.PSParameterizedProperty'. Path 'message.toRecipients[0].emailAddress.address.Chars'.

This is surprising because from PowerShell the value still reports as System.String:

$payload.message.toRecipients[0].emailAddress.address.GetType().FullName
# System.String

But from .NET, the dictionary value is actually a System.Management.Automation.PSObject whose BaseObject is a System.String.

Expected behavior

Invoke-MgGraphRequest -Body should serialize idiomatic PowerShell hashtable bodies containing pipeline-produced primitive values the same way it serializes directly assigned primitive values.

At minimum, string-like PSObject wrappers should be unwrapped before JSON serialization so callers do not need to know about this PowerShell-to-.NET interop boundary.

How to reproduce

The public Invoke-MgGraphRequest path requires an authenticated Graph context before it reaches body serialization. The following repro invokes the same internal SetRequestContent(HttpRequestMessage, IDictionary) path via reflection so it can be run without a tenant or token.

Import-Module Microsoft.Graph.Authentication

$type = [Microsoft.Graph.PowerShell.Authentication.Cmdlets.InvokeMgGraphRequest]
$method = $type.GetMethod(
    'SetRequestContent',
    [System.Reflection.BindingFlags]'NonPublic,Instance',
    $null,
    @([System.Net.Http.HttpRequestMessage], [System.Collections.IDictionary]),
    $null)
$cmdlet = [Activator]::CreateInstance($type)

$toAddresses = @('recipient@example.com' -split '[;,]' |
    ForEach-Object { $_.Trim() } |
    Where-Object { $_ })

# This is the problematic, idiomatic PowerShell shape:
$body = @{
    message = @{
        toRecipients = @(
            $toAddresses | ForEach-Object {
                @{ emailAddress = @{ address = $_ } }
            }
        )
    }
    saveToSentItems = $true
}

$request = [System.Net.Http.HttpRequestMessage]::new(
    [System.Net.Http.HttpMethod]::Post,
    'https://graph.microsoft.com/v1.0/users/sender@example.com/sendMail')

$method.Invoke($cmdlet, @($request, $body))

Actual result:

Exception calling "Invoke" with "2" argument(s): "Self referencing loop detected for property 'Value' with type 'System.Management.Automation.PSParameterizedProperty'. Path 'message.toRecipients[0].emailAddress.address.Chars'."

If the address assignment is changed to explicitly cast the pipeline value, serialization succeeds:

@{ emailAddress = @{ address = [string]$_ } }

With that cast, the serialized body is:

{"saveToSentItems":true,"message":{"toRecipients":[{"emailAddress":{"address":"recipient@example.com"}}]}}

A small probe showing the type mismatch at the .NET boundary:

Add-Type -ReferencedAssemblies ([System.Management.Automation.PSObject].Assembly.Location) -TypeDefinition @'
using System;
using System.Collections;
using System.Management.Automation;
public static class DictProbe {
    public static string DescribeAddress(IDictionary body) {
        var message = (IDictionary)body["message"];
        var recipients = (IEnumerable)message["toRecipients"];
        object firstRecipient = null;
        foreach (var item in recipients) { firstRecipient = item; break; }
        var recipient = (IDictionary)firstRecipient;
        var email = (IDictionary)recipient["emailAddress"];
        var address = email["address"];
        var s = "dotnetType=" + address.GetType().FullName;
        if (address is PSObject p) {
            s += "; baseType=" + p.BaseObject.GetType().FullName;
            s += "; charsMemberType=" + p.Members["Chars"].GetType().FullName;
            s += "; charsMemberKind=" + p.Members["Chars"].MemberType;
        }
        return s;
    }
}
'@

[DictProbe]::DescribeAddress($body)

Output for the failing body:

dotnetType=System.Management.Automation.PSObject; baseType=System.String; charsMemberType=System.Management.Automation.PSParameterizedProperty; charsMemberKind=ParameterizedProperty

Output after using [string]$_:

dotnetType=System.String

SDK Version

Reproduced with:

Microsoft.Graph.Authentication 2.25.0
Microsoft.Graph.Authentication 2.35.1

The current source also appears to still call Newtonsoft directly for dictionary bodies:

// InvokeMgGraphRequest.SetRequestContent(HttpRequestMessage request, IDictionary content)
var body = JsonConvert.SerializeObject(content);

Latest version known to work for scenario above?

None known.

Known Workarounds

Explicitly cast pipeline-derived primitive values before placing them into the hashtable body, e.g.:

$toAddresses | ForEach-Object {
    @{ emailAddress = @{ address = [string]$_ } }
}

or otherwise ensure values crossing into Invoke-MgGraphRequest -Body are plain CLR primitives rather than PSObject-wrapped values.

Debug output

No Graph debug output is produced in the repro above because the failure happens during request content serialization, before the HTTP request is sent.

Configuration

Local repro environment:

PowerShell 7.6.3
Debian GNU/Linux 13 (trixie)
x64

Other information

This looks like a PowerShell/.NET interop robustness issue in Invoke-MgGraphRequest's body serializer rather than a Graph API issue. PowerShell callers can construct a normal-looking hashtable where .GetType() reports System.String, but a generic .NET serializer sees a PSObject wrapper and walks PowerShell adapted members such as Chars.

Related prior art for this general failure class exists outside the Graph SDK, e.g. serialization of raw PowerShell objects hitting circular reference errors involving PSParameterizedProperty.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions