diff --git a/dotnet/src/Microsoft.Agents.AI/JsonFixer.cs b/dotnet/src/Microsoft.Agents.AI/JsonFixer.cs
new file mode 100644
index 0000000000..dc7804209b
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI/JsonFixer.cs
@@ -0,0 +1,207 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace Microsoft.Agents.AI;
+
+///
+/// Provides utility methods for fixing common JSON malformations
+/// that can arise when consuming JSON output from LLMs.
+///
+internal static class JsonFixer
+{
+ ///
+ /// Attempts to fix common JSON malformations in the provided text.
+ ///
+ /// The raw text potentially containing JSON.
+ /// The repaired JSON text, or the original if no fix was needed.
+ /// if a fix was applied; if the text was already valid or no fix was possible.
+ public static bool TryFix([NotNullWhen(true)] string? text, out string? fixedText)
+ {
+ if (string.IsNullOrEmpty(text))
+ {
+ fixedText = null;
+ return false;
+ }
+
+ string result = text;
+
+ bool changed = TryStripMarkdownFences(ref result)
+ | TryFixTrailingCommas(ref result)
+ | TryFixTruncatedJson(ref result)
+ | TryUnstringifyNestedJson(ref result);
+
+ fixedText = changed ? result : null;
+ return changed;
+ }
+
+ ///
+ /// Removes markdown code fences (e.g. ```json ... ```) from the text.
+ ///
+ public static bool TryStripMarkdownFences(ref string text)
+ {
+ const string FenceMarker = "```";
+ int start = text.IndexOf(FenceMarker, StringComparison.Ordinal);
+ if (start < 0)
+ {
+ return false;
+ }
+
+ // Find the end of the fence line (the newline after the opening fence)
+ int fenceEnd = text.IndexOf('\n', start);
+ if (fenceEnd < 0)
+ {
+ // ``` at start but no newline — treat rest as code
+ text = text[(start + 3)..].Trim();
+ return true;
+ }
+
+ int contentStart = fenceEnd + 1;
+
+ // Find closing fence
+ int closeFence = text.LastIndexOf(FenceMarker, StringComparison.Ordinal);
+ if (closeFence >= contentStart)
+ {
+ // Extract content between fences
+ text = text[contentStart..closeFence].Trim();
+ }
+ else
+ {
+ // No closing fence — treat rest as code
+ text = text[contentStart..].Trim();
+ }
+
+ return true;
+ }
+
+ ///
+ /// Removes trailing commas before '}', ']', or at the end of the string.
+ ///
+ public static bool TryFixTrailingCommas(ref string text)
+ {
+ string original = text;
+
+ // Remove trailing comma before closing brace/bracket
+ text = Regex.Replace(text, @",(\s*[}\]])", "$1");
+
+ // Remove trailing comma at end of string (truncated after comma)
+ text = Regex.Replace(text, @",\s*$", "");
+
+ return text != original;
+ }
+
+ ///
+ /// Attempts to complete a truncated JSON payload by adding missing closing brackets,
+ /// braces, and quotes.
+ ///
+ public static bool TryFixTruncatedJson(ref string text)
+ {
+ string original = text;
+ var stack = new Stack();
+ bool inString = false;
+ bool escaped = false;
+
+ foreach (char c in text)
+ {
+ if (inString)
+ {
+ if (escaped)
+ {
+ escaped = false;
+ }
+ else if (c == '\\')
+ {
+ escaped = true;
+ }
+ else if (c == '"')
+ {
+ inString = false;
+ }
+ }
+ else
+ {
+ switch (c)
+ {
+ case '{':
+ case '[':
+ stack.Push(c);
+ break;
+ case '}':
+ if (stack.Count > 0 && stack.Peek() == '{')
+ {
+ stack.Pop();
+ }
+ break;
+ case ']':
+ if (stack.Count > 0 && stack.Peek() == '[')
+ {
+ stack.Pop();
+ }
+ break;
+ case '"':
+ inString = true;
+ break;
+ }
+ }
+ }
+
+ // Close any unclosed string
+ if (inString)
+ {
+ text += '"';
+ }
+
+ // Close any unclosed brackets/braces
+ while (stack.Count > 0)
+ {
+ text += stack.Pop() switch
+ {
+ '{' => '}',
+ '[' => ']',
+ _ => string.Empty
+ };
+ }
+
+ return text != original;
+ }
+
+ ///
+ /// Detects and un-stringifies nested JSON objects that have been embedded
+ /// as escaped string values (e.g. "arguments": "{\"key\": \"value\"}"
+ /// becomes "arguments": {"key": "value"}).
+ ///
+ public static bool TryUnstringifyNestedJson(ref string text)
+ {
+ string original = text;
+
+ // Match pattern: "propertyName": "{...}" or "propertyName": "{\\...}"
+ text = Regex.Replace(
+ text,
+ @"\""(\w+)\\"":\s*\""(\{.*?\})\""",
+ m =>
+ {
+ string propertyName = m.Groups[1].Value;
+ string potentialJson = m.Groups[2].Value;
+
+ // Unescape the string
+ potentialJson = Regex.Unescape(potentialJson);
+
+ // Check if it's valid JSON
+ try
+ {
+ System.Text.Json.JsonDocument.Parse(potentialJson);
+ // It's valid JSON, so use it directly
+ return $"\"{propertyName}\": {potentialJson}";
+ }
+ catch
+ {
+ // Not valid JSON, keep original
+ return m.Value;
+ }
+ });
+
+ return text != original;
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/JsonFixerTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/JsonFixerTests.cs
new file mode 100644
index 0000000000..ecf2b4bd9d
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/JsonFixerTests.cs
@@ -0,0 +1,157 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Xunit;
+
+namespace Microsoft.Agents.AI.UnitTests;
+
+///
+/// Tests for .
+///
+public class JsonFixerTests
+{
+ // ---------- Markdown Fence Stripping ----------
+
+ [Fact]
+ public void TryStripMarkdownFences_NoFence_ReturnsFalse()
+ {
+ string text = @"{""key"": ""value""}";
+ string original = text;
+ bool result = JsonFixer.TryStripMarkdownFences(ref text);
+ Assert.False(result);
+ Assert.Equal(original, text);
+ }
+
+ [Fact]
+ public void TryStripMarkdownFences_WithFence_RemovesFence()
+ {
+ string text = "```json\n{\"key\": \"value\"}\n```";
+ string expected = @"{""key"": ""value""}";
+ bool result = JsonFixer.TryStripMarkdownFences(ref text);
+ Assert.True(result);
+ Assert.Equal(expected, text);
+ }
+
+ [Fact]
+ public void TryStripMarkdownFences_NoClosingFence_StripsOpeningFence()
+ {
+ string text = "```json\n{\"key\": \"value\"}";
+ string expected = @"{""key"": ""value""}";
+ bool result = JsonFixer.TryStripMarkdownFences(ref text);
+ Assert.True(result);
+ Assert.Equal(expected, text);
+ }
+
+ // ---------- Trailing Comma Fixing ----------
+
+ [Fact]
+ public void TryFixTrailingCommas_NoTrailingComma_ReturnsFalse()
+ {
+ string text = @"{""a"": 1, ""b"": 2}";
+ string original = text;
+ bool result = JsonFixer.TryFixTrailingCommas(ref text);
+ Assert.False(result);
+ Assert.Equal(original, text);
+ }
+
+ [Fact]
+ public void TryFixTrailingCommas_BeforeClosingBrace_RemovesComma()
+ {
+ string text = @"{""a"": 1,}";
+ string expected = @"{""a"": 1}";
+ bool result = JsonFixer.TryFixTrailingCommas(ref text);
+ Assert.True(result);
+ Assert.Equal(expected, text);
+ }
+
+ [Fact]
+ public void TryFixTrailingCommas_BeforeClosingBracket_RemovesComma()
+ {
+ string text = @"[1, 2,]";
+ string expected = @"[1, 2]";
+ bool result = JsonFixer.TryFixTrailingCommas(ref text);
+ Assert.True(result);
+ Assert.Equal(expected, text);
+ }
+
+ // ---------- Truncated JSON Fixing ----------
+
+ [Fact]
+ public void TryFixTruncatedJson_CompleteJson_ReturnsFalse()
+ {
+ string text = @"{""a"": 1, ""b"": [1, 2, 3]}";
+ string original = text;
+ bool result = JsonFixer.TryFixTruncatedJson(ref text);
+ Assert.False(result);
+ Assert.Equal(original, text);
+ }
+
+ [Fact]
+ public void TryFixTruncatedJson_MissingClosingBrace_AddsIt()
+ {
+ string text = @"{""a"": 1";
+ string expected = @"{""a"": 1}";
+ bool result = JsonFixer.TryFixTruncatedJson(ref text);
+ Assert.True(result);
+ Assert.Equal(expected, text);
+ }
+
+ [Fact]
+ public void TryFixTruncatedJson_MissingBracketsAndBraces_AddsThem()
+ {
+ string text = @"{""a"": [1, 2";
+ string expected = @"{""a"": [1, 2]}";
+ bool result = JsonFixer.TryFixTruncatedJson(ref text);
+ Assert.True(result);
+ Assert.Equal(expected, text);
+ }
+
+ [Fact]
+ public void TryFixTruncatedJson_UnclosedString_ClosesIt()
+ {
+ string text = @"{""a"": ""hello";
+ string expected = @"{""a"": ""hello""}";
+ bool result = JsonFixer.TryFixTruncatedJson(ref text);
+ Assert.True(result);
+ Assert.Equal(expected, text);
+ }
+
+ // ---------- Combined TryFix ----------
+
+ [Fact]
+ public void TryFix_MarkdownFenceWithCommas_FixesBoth()
+ {
+ string text = "```json\n{\"a\": 1,}\n```";
+ string expected = @"{""a"": 1}";
+ bool result = JsonFixer.TryFix(text, out string? fixedText);
+ Assert.True(result);
+ Assert.NotNull(fixedText);
+ Assert.Equal(expected, fixedText);
+ }
+
+ [Fact]
+ public void TryFix_TruncatedWithFence_FixesBoth()
+ {
+ string text = "```json\n{\"a\": [1, 2";
+ string expected = @"{""a"": [1, 2]}";
+ bool result = JsonFixer.TryFix(text, out string? fixedText);
+ Assert.True(result);
+ Assert.NotNull(fixedText);
+ Assert.Equal(expected, fixedText);
+ }
+
+ [Fact]
+ public void TryFix_NullText_ReturnsFalse()
+ {
+ bool result = JsonFixer.TryFix(null, out string? fixedText);
+ Assert.False(result);
+ Assert.Null(fixedText);
+ }
+
+ [Fact]
+ public void TryFix_EmptyText_ReturnsFalse()
+ {
+ bool result = JsonFixer.TryFix(string.Empty, out string? fixedText);
+ Assert.False(result);
+ Assert.Null(fixedText);
+ }
+}