If you’re building automation on top of Microsoft Graph — reporting scripts, an Intune dashboard, a compliance tool — you’ll eventually run into a class of bugs that don’t show up in the docs and don’t throw the error you’d expect. They’re not bugs in your code; they’re mismatches between what Graph documents and what Graph actually returns. Here are three I ran into building CET2, my compliance and endpoint reporting platform, and how I caught them.
1. PowerShell silently rewrites your date strings
I had a script using Invoke-RestMethod to poll a Graph report-export job status. Part of the logic checked for a sentinel string value in a date field — comparing a completionDateTime field against a known “not yet finished” marker.
It worked fine in testing. It failed in production, intermittently, in a way that looked like a race condition.
The actual cause: Invoke-RestMethod in PowerShell automatically deserialises JSON strings that look like ISO 8601 dates into [DateTime] objects. My sentinel value was a valid-looking ISO date string, so PowerShell “helpfully” converted it before my string-equality check ever ran. I wasn’t comparing strings any more — I was comparing a [DateTime] to a string, silently coercing types, and getting unpredictable results depending on culture settings and how PowerShell felt like comparing that day.
# looks safe — isn't
if ($job.completionDateTime -eq $STILL_RUNNING_MARKER) { ... }
# PowerShell has already turned the JSON string into [DateTime]
$job.completionDateTime.GetType().Name # → DateTime, not StringThe fix: don’t rely on string comparisons against anything that resembles a date coming back from Invoke-RestMethod. Either use Invoke-WebRequest and parse the JSON yourself with ConvertFrom-Json -AsHashtable (which won’t auto-type dates), or compare against [DateTime] explicitly and stop assuming the wire format is what you’ll get in the pipeline.
2. jailBroken is a string, not a boolean
The managedDevice resource in Graph has a field called jailBroken. If you’re writing a typed client — a C# model, a strongly-typed PowerShell class, anything with schema validation — your instinct is to type it as bool?, because that’s what the name implies.
It isn’t. Graph returns jailBroken as a string: "True", "False", or "Unknown".
I found this the hard way when a device detail page in CET2 started throwing 500s for a specific subset of devices. The stack trace pointed at System.Text.Json failing on deserialisation — a jailbroken (or more often, unknown-status) device would come back with "Unknown" in that field, which isn’t a valid boolean literal, and the whole deserialisation fell over instead of failing gracefully on one field.
// what the field name suggests
public bool? JailBroken { get; set; }
// what Graph actually sends
// "jailBroken": "Unknown" ← throws on deserialiseThe fix: type it as a string (or a tri-state enum: True / False / Unknown) and convert at the boundary where you actually need a boolean. Don’t trust field names in the Graph schema to tell you the real JSON type — check an actual response payload, not just the docs, before you commit to a model type.
3. “Not applicable” isn’t an empty result — it’s a BadRequest
I run scheduled report-export jobs against the Windows Update report-export APIs to pull compliance and deployment data across tenants. The reasonable assumption is that a tenant with no relevant data returns an empty result set. That’s how most Graph reporting endpoints behave.
Not this one. For tenants that haven’t configured Windows Update rings or feature update policies — i.e. the report genuinely doesn’t apply to them — the API doesn’t return an empty array. It throws a BadRequest. From the caller’s side, that’s indistinguishable from “you sent a malformed request” or “something’s actually broken”, unless you already know this particular endpoint behaves that way for that reason.
This matters at scale: if you’re polling across dozens or hundreds of tenants on a schedule, a fair chunk of them will legitimately have nothing configured, and your job needs to treat that as a normal, expected outcome rather than an error to retry or alert on.
The fix: I added tenant-level state tracking — a flag on the tenant record noting “this report type doesn’t apply here, don’t ask again for 24 hours.” That turns a repeated, alert-triggering failure into a single classified state and a scheduled recheck, rather than noisy false-positive errors clogging up monitoring on every run.
The pattern underneath all three
None of these are exotic edge cases — they’re the sort of thing you only find by running against real, messy production tenant data instead of a clean test tenant, and by actually reading raw response payloads rather than trusting the documented schema. If you’re building anything non-trivial on Graph:
- Log and inspect raw JSON responses during development — don’t just deserialise and move on.
- Treat every “boolean-sounding” field as suspect until you’ve seen it in a real payload.
- Design your error handling to distinguish “this failed” from “this doesn’t apply”, because Graph often won’t distinguish them for you.
- Assume date/time fields will get mangled by whatever HTTP client you’re using, unless you’ve verified otherwise.
I keep hitting these because Graph covers an enormous surface area across products with inconsistent conventions between endpoints. If you’re building Intune or M365 tooling of your own, it’s worth budgeting time for exactly this kind of discovery — it doesn’t show up in a spec review, only in production.