Skip to content

Add System.Text.Json support to AdaptiveCards .NET library #9191

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 20 additions & 13 deletions source/dotnet/Library/AdaptiveCards/AdaptiveCard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -286,23 +286,20 @@ public static AdaptiveCardParseResult FromJson(string json)

try
{
parseResult.Card = JsonConvert.DeserializeObject<AdaptiveCard>(json, new JsonSerializerSettings
var options = new System.Text.Json.JsonSerializerOptions
{
ContractResolver = new WarningLoggingContractResolver(parseResult, new ParseContext()),
Converters = { new StrictIntConverter() },
Error = delegate (object sender, ErrorEventArgs args)
{
if (args.ErrorContext.Error.GetType() == typeof(JsonSerializationException))
{
args.ErrorContext.Handled = true;
}
}
});
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true
};

var dto = System.Text.Json.JsonSerializer.Deserialize<AdaptiveCards.SystemTextJson.AdaptiveCardDto>(json, options);
parseResult.Card = AdaptiveCards.SystemTextJson.AdaptiveCardDtoConverter.FromDto(dto);
}
catch (JsonException ex)
catch (System.Text.Json.JsonException ex)
{
throw new AdaptiveSerializationException(ex.Message, ex);
}

return parseResult;
}

Expand All @@ -312,9 +309,19 @@ public static AdaptiveCardParseResult FromJson(string json)
/// <returns>The JSON representation of this AdaptiveCard.</returns>
public string ToJson()
{
return JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented);
var dto = AdaptiveCards.SystemTextJson.AdaptiveCardDtoConverter.ToDto(this);

var options = new System.Text.Json.JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
};

return System.Text.Json.JsonSerializer.Serialize(dto, options);
}


/// <summary>
/// Get resource information for all images and media present in this card.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace AdaptiveCards
{
/// <summary>
/// Helper class used by System.Text.Json to convert an AdaptiveCard to/from JSON.
/// </summary>
public class AdaptiveCardSystemTextJsonConverter : AdaptiveTypedBaseElementSystemTextJsonConverter<AdaptiveCard>, ILogWarnings
{
/// <summary>
/// A list of warnings generated by the converter.
/// </summary>
public List<AdaptiveWarning> Warnings { get; set; } = new List<AdaptiveWarning>();

/// <summary>
/// Reads JSON and converts it to an AdaptiveCard.
/// </summary>
public override AdaptiveCard Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using (JsonDocument document = JsonDocument.ParseValue(ref reader))
{
JsonElement root = document.RootElement;

if (!root.TryGetProperty("type", out JsonElement typeElement) ||
typeElement.GetString() != AdaptiveCard.TypeName)
{
throw new AdaptiveSerializationException($"Property 'type' must be '{AdaptiveCard.TypeName}'");
}

// Validate version (similar to original converter)
ValidateJsonVersion(root);

// Check for fallback scenario
if (root.TryGetProperty("version", out JsonElement versionElement))
{
string versionString = versionElement.GetString();
if (!string.IsNullOrEmpty(versionString) &&
new AdaptiveSchemaVersion(versionString) > AdaptiveCard.KnownSchemaVersion)
{
return MakeFallbackTextCard(root);
}
}

// Create a new AdaptiveCard and populate its properties
AdaptiveCard card = CreateCardFromJsonElement(root, options);

// Validate and set language
if (root.TryGetProperty("lang", out JsonElement langElement))
{
card.Lang = ValidateLang(langElement.GetString());
}

return card;
}
}

/// <summary>
/// Writes an AdaptiveCard to JSON.
/// </summary>
public override void Write(Utf8JsonWriter writer, AdaptiveCard value, JsonSerializerOptions options)
{
// For now, we'll use the default serialization behavior
// This can be enhanced later to match the exact format of Newtonsoft.Json
JsonSerializer.Serialize(writer, value, value.GetType(), options);
}

private void ValidateJsonVersion(JsonElement root)
{
string exceptionMessage = "";

if (!root.TryGetProperty("version", out JsonElement versionElement))
{
exceptionMessage = "Could not parse required key: version. It was not found.";
}
else
{
string version = versionElement.GetString();
if (string.IsNullOrEmpty(version))
{
exceptionMessage = "Property is required but was found empty: version";
}
}

if (!string.IsNullOrEmpty(exceptionMessage))
{
if (AdaptiveCard.OnDeserializingMissingVersion == null)
{
throw new AdaptiveSerializationException(exceptionMessage);
}
else
{
// This is a limitation - we can't modify the JsonElement like we could with JObject
// The caller will need to handle this scenario differently for System.Text.Json
var overriddenVersion = AdaptiveCard.OnDeserializingMissingVersion();
// Note: We can't modify the JSON element, so this requires a different approach
}
}
}

private AdaptiveCard CreateCardFromJsonElement(JsonElement root, JsonSerializerOptions options)
{
// Extract version
string version = "1.0"; // default
if (root.TryGetProperty("version", out JsonElement versionElement))
{
version = versionElement.GetString() ?? "1.0";
}

AdaptiveCard card = new AdaptiveCard(version);

// Set basic properties
if (root.TryGetProperty("fallbackText", out JsonElement fallbackTextElement))
{
card.FallbackText = fallbackTextElement.GetString();
}

if (root.TryGetProperty("speak", out JsonElement speakElement))
{
card.Speak = speakElement.GetString();
}

// TODO: Handle other properties like body, actions, backgroundImage, etc.
// This is a simplified implementation to start with

return card;
}

private string ValidateLang(string val)
{
if (!string.IsNullOrEmpty(val))
{
try
{
if (val.Length == 2 || val.Length == 3)
{
new CultureInfo(val);
}
else
{
Warnings.Add(new AdaptiveWarning((int)AdaptiveWarning.WarningStatusCode.InvalidLanguage, "Invalid language identifier: " + val));
}
}
catch (CultureNotFoundException)
{
Warnings.Add(new AdaptiveWarning((int)AdaptiveWarning.WarningStatusCode.InvalidLanguage, "Invalid language identifier: " + val));
}
}
return val;
}

private AdaptiveCard MakeFallbackTextCard(JsonElement root)
{
// Retrieve values defined by parsed json
string fallbackText = null;
string speak = null;
string language = null;

if (root.TryGetProperty("fallbackText", out JsonElement fallbackTextElement))
{
fallbackText = fallbackTextElement.GetString();
}

if (root.TryGetProperty("speak", out JsonElement speakElement))
{
speak = speakElement.GetString();
}

if (root.TryGetProperty("lang", out JsonElement langElement))
{
language = langElement.GetString();
}

// Replace undefined values by default values
if (string.IsNullOrEmpty(fallbackText))
{
fallbackText = "We're sorry, this card couldn't be displayed";
}
if (string.IsNullOrEmpty(speak))
{
speak = fallbackText;
}
if (string.IsNullOrEmpty(language))
{
language = CultureInfo.CurrentCulture.TwoLetterISOLanguageName;
}

// Define AdaptiveCard to return
AdaptiveCard fallbackCard = new AdaptiveCard("1.0")
{
Speak = speak,
Lang = language
};
fallbackCard.Body.Add(new AdaptiveTextBlock
{
Text = fallbackText
});

// Add relevant warning
Warnings.Add(new AdaptiveWarning((int)AdaptiveWarning.WarningStatusCode.UnsupportedSchemaVersion, "Schema version is not supported"));

return fallbackCard;
}
}
}
1 change: 1 addition & 0 deletions source/dotnet/Library/AdaptiveCards/AdaptiveCards.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@

<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.*" />
<PackageReference Include="System.Net.Http" Version="4.3.*" />
<PackageReference Include="Vsxmd" Version="1.4.*" Condition="$(TargetFramework) == 'uap10.0'">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace AdaptiveCards
{
/// <summary>
/// System.Text.Json converter for AdaptiveSchemaVersion to ensure it serializes as a string (e.g. "1.0")
/// instead of an object with major/minor properties.
/// </summary>
public class AdaptiveSchemaVersionSystemTextJsonConverter : JsonConverter<AdaptiveSchemaVersion>
{
/// <summary>
/// Reads a version string and converts it to AdaptiveSchemaVersion.
/// </summary>
public override AdaptiveSchemaVersion Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
string versionString = reader.GetString();
return new AdaptiveSchemaVersion(versionString);
}
else if (reader.TokenType == JsonTokenType.StartObject)
{
// Handle object format like {"major": 1, "minor": 0}
using (JsonDocument document = JsonDocument.ParseValue(ref reader))
{
JsonElement root = document.RootElement;

int major = 1;
int minor = 0;

if (root.TryGetProperty("major", out JsonElement majorElement))
{
major = majorElement.GetInt32();
}

if (root.TryGetProperty("minor", out JsonElement minorElement))
{
minor = minorElement.GetInt32();
}

return new AdaptiveSchemaVersion(major, minor);
}
}

throw new JsonException($"Unable to parse AdaptiveSchemaVersion from {reader.TokenType}");
}

/// <summary>
/// Writes AdaptiveSchemaVersion as a string (e.g. "1.0").
/// </summary>
public override void Write(Utf8JsonWriter writer, AdaptiveSchemaVersion value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Text.Json;
using System.Text.Json.Serialization;

namespace AdaptiveCards
{
/// <summary>
/// System.Text.Json converters that deserialize to AdaptiveCards elements and use ParseContext must inherit this class.
/// ParseContext provides id generation, id collision detections, and other useful services during deserialization.
/// </summary>
public abstract class AdaptiveTypedBaseElementSystemTextJsonConverter<T> : JsonConverter<T>
{
/// <summary>
/// The <see cref="ParseContext"/> to use while parsing in AdaptiveCards.
/// </summary>
public ParseContext ParseContext { get; set; } = new ParseContext();
}
}
Loading
Loading