< Summary - AsyncResponse (Release / net8.0+net10.0 / unit+integration)

Information
Class: AsyncResponse.JsonSafety
Assembly: AsyncResponse.Core
File(s): /home/runner/work/AsyncResponse/AsyncResponse/src/AsyncResponse.Core/JsonSafety.cs
Line coverage
100%
Covered lines: 23
Uncovered lines: 0
Coverable lines: 23
Total lines: 97
Line coverage: 100%
Branch coverage
100%
Covered branches: 10
Total branches: 10
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
SafeDeserialize<T>(...)100%11100%
SafeDeserialize<T>(...)100%11100%
SafeDeserialize(...)100%11100%
WithResolver(...)100%66100%
PopulateReflectionResolver()100%11100%
ThrowIfClearlyNotJson(...)100%44100%

File(s)

/home/runner/work/AsyncResponse/AsyncResponse/src/AsyncResponse.Core/JsonSafety.cs

#LineLine coverage
 1using System.Diagnostics.CodeAnalysis;
 2using System.Text.Json;
 3using System.Text.Json.Serialization.Metadata;
 4
 5namespace AsyncResponse;
 6
 7/// <summary>
 8/// Defensive JSON helpers for broker ingress payloads. All overloads resolve contract metadata
 9/// through <see cref="AsyncResponseJson"/> (trim/AOT-safe) instead of the reflection-based
 10/// serializer entry points; property matching stays case-insensitive as it always was here.
 11/// </summary>
 12internal static class JsonSafety
 13{
 14    /// <summary>
 15    /// Deserializes with guards for the classic broker-ingress garbage: empty bodies and HTML
 16    /// error pages. Throws <see cref="InvalidDataException"/> with the offending prefix so the
 17    /// failure is diagnosable from logs.
 18    /// </summary>
 19    public static T? SafeDeserialize<T>(string json, JsonSerializerOptions? options = null)
 320        => SafeDeserialize(json, AsyncResponseJson.GetTypeInfo<T>(WithResolver(options)));
 21
 22    /// <summary>Deserializes with the ingress guards using pre-resolved contract metadata.</summary>
 23    public static T? SafeDeserialize<T>(string json, JsonTypeInfo<T> typeInfo)
 24    {
 325        ThrowIfClearlyNotJson(json);
 26
 27        try
 28        {
 329            return JsonSerializer.Deserialize(json, typeInfo);
 30        }
 331        catch (JsonException jsonException)
 32        {
 33            // Re-throw with the payload prefix in the message so the failure is diagnosable.
 334            throw new InvalidDataException($"Failed to parse JSON payload: {json[..Math.Min(200, json.Length)]}…", jsonE
 35        }
 336    }
 37
 38    /// <summary>
 39    /// Non-generic counterpart for callers that only know the target type at runtime (e.g.
 40    /// materializing a persisted flow input).
 41    /// </summary>
 42    public static object? SafeDeserialize(string json, Type returnType, JsonSerializerOptions? options = null)
 43    {
 344        ThrowIfClearlyNotJson(json);
 45
 46        try
 47        {
 248            return JsonSerializer.Deserialize(json, AsyncResponseJson.GetTypeInfo(returnType, WithResolver(options)));
 49        }
 250        catch (JsonException jsonException)
 51        {
 52            // Re-throw with the payload prefix in the message so the failure is diagnosable.
 253            throw new InvalidDataException($"Failed to parse JSON payload: {json[..Math.Min(200, json.Length)]}…", jsonE
 54        }
 255    }
 56
 57    /// <summary>
 58    /// Defaults to the library's case-insensitive chain options. Caller-supplied options are
 59    /// honored exactly as the reflection-based overloads honored them: an instance with no
 60    /// resolver gets the runtime's default reflection resolver bound (that is what
 61    /// <c>JsonSerializer.Deserialize(json, options)</c> used to do on first use), which throws at
 62    /// runtime when the app disabled reflection-based serialization — same as before, but without
 63    /// carrying IL2026/IL3050.
 64    /// </summary>
 65    private static JsonSerializerOptions WithResolver(JsonSerializerOptions? options)
 66    {
 367        if (options is null)
 368            return AsyncResponseJson.CaseInsensitive;
 69
 70        // When reflection is unavailable (trimmed/AOT) the resolver stays null and GetTypeInfo
 71        // surfaces the actionable register-a-context error instead.
 372        if (options.TypeInfoResolver is null && JsonSerializer.IsReflectionEnabledByDefault)
 273            PopulateReflectionResolver(options);
 74
 275        return options;
 76
 77        [UnconditionalSuppressMessage("Trimming", "IL2026",
 78            Justification = "Reachable only when JsonSerializer.IsReflectionEnabledByDefault is true; trimmed and AOT bu
 79        [UnconditionalSuppressMessage("AOT", "IL3050",
 80            Justification = "Same guard: never reached under Native AOT.")]
 81        static void PopulateReflectionResolver(JsonSerializerOptions options)
 382            => options.MakeReadOnly(populateMissingResolver: true);
 83    }
 84
 85    /// <summary>Runs the ThrowIfClearlyNotJson operation.</summary>
 86    public static void ThrowIfClearlyNotJson(string json)
 87    {
 388        if (string.IsNullOrWhiteSpace(json))
 389            throw new InvalidDataException("Empty message body when JSON was expected.");
 90
 391        var trimmed = json.AsSpan().TrimStart();
 92
 93        // Guard against HTML error pages.
 394        if (trimmed[0] == '<')
 395            throw new InvalidDataException($"Received HTML when JSON was expected: {json[..Math.Min(200, json.Length)]}…
 396    }
 97}