using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace PCL.Core.Utils; /// /// System.Text.Json 兼容 Newtonsoft.Json 宽松行为的统一入口。 /// public static class JsonCompat { public static readonly JsonNodeOptions NodeOptions = new() { PropertyNameCaseInsensitive = true }; public static readonly JsonDocumentOptions DocumentOptions = new() { AllowTrailingCommas = true, CommentHandling = JsonCommentHandling.Skip }; /// /// 统一的宽松 JSON 序列化配置。该实例在静态初始化时已被冻结,调用方不能修改全局行为。 /// 如需追加调用点专用设置,请使用 new JsonSerializerOptions(JsonCompat.SerializerOptions) 克隆后修改。 /// public static JsonSerializerOptions SerializerOptions { get; } = _CreateSerializerOptions(); private static JsonSerializerOptions _CreateSerializerOptions() { var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, Converters = { new FlexibleDateTimeConverter(), new FlexibleBoolConverter(), new FlexibleStringConverter(), new JsonStringEnumConverter() } }; options.MakeReadOnly(true); return options; } public static JsonNode ParseNode(string text) { return JsonNode.Parse(text, NodeOptions, DocumentOptions)!; } public static T? ToObject(this JsonNode? node) { return node is null ? default : node.Deserialize(SerializerOptions); } public static JsonArray FromObject(IEnumerable items) { var arr = new JsonArray(); foreach (var item in items) arr.Add(JsonSerializer.SerializeToNode(item, SerializerOptions)); return arr; } public static bool TryGetDateTime(JsonNode? node, out DateTime dateTime) { dateTime = default; switch (node) { case null: return false; case JsonValue value when value.TryGetValue(out var rawDateTime): dateTime = NormalizeDateTime(rawDateTime); return true; case JsonValue value when value.TryGetValue(out var rawText) && TryParseDateTime(rawText, out dateTime): return true; default: try { dateTime = NormalizeDateTime(node.Deserialize(SerializerOptions)); return true; } catch (JsonException) { return false; } catch (InvalidOperationException) { return false; } catch (NotSupportedException) { return false; } } } public static bool TryParseDateTime(string? value, out DateTime dateTime) { dateTime = default; if (string.IsNullOrWhiteSpace(value)) return false; // ISO 8601 允许用 24:00:00 表示当日终点(语义等于次日零点),但 .NET 的日期解析器不接受小时为 24, // 需先把小时 24 归一化为 00,再在解析成功后补一天。例如社区版本清单中的 2009-10-24T24:00:00+00:00。 var addDay = false; var endOfDay = RegexPatterns.Iso8601EndOfDay.Match(value); if (endOfDay.Success) { value = value.Remove(endOfDay.Index + 1, 2).Insert(endOfDay.Index + 1, "00"); addDay = true; } if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var dateTimeOffset)) { if (addDay) dateTimeOffset = dateTimeOffset.AddDays(1); dateTime = dateTimeOffset.LocalDateTime; return true; } if (!DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var parsed)) return false; dateTime = NormalizeDateTime(addDay ? parsed.AddDays(1) : parsed); return true; } public static DateTime NormalizeDateTime(DateTime dateTime) { return dateTime.Kind == DateTimeKind.Utc ? dateTime.ToLocalTime() : dateTime; } public static void Merge(this JsonObject target, JsonNode? source) { if (source is not JsonObject sourceObj) return; foreach (var prop in sourceObj.ToArray()) switch (target[prop.Key]) { case JsonObject targetChild when prop.Value is JsonObject sourceChild: targetChild.Merge(sourceChild); break; case JsonArray targetArray when prop.Value is JsonArray sourceArray: targetArray.Merge(sourceArray); break; default: target[prop.Key] = prop.Value?.DeepClone(); break; } } public static void Merge(this JsonArray target, JsonNode? source) { if (source is not JsonArray sourceArr) return; foreach (var item in sourceArr) target.Add(item?.DeepClone()); } private sealed class FlexibleDateTimeConverter : JsonConverter { public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType != JsonTokenType.String) return NormalizeDateTime(reader.GetDateTime()); var value = reader.GetString(); return TryParseDateTime(value, out var dateTime) ? dateTime : NormalizeDateTime(reader.GetDateTime()); } public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) { writer.WriteStringValue(value); } } private sealed class FlexibleBoolConverter : JsonConverter { public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return reader.TokenType switch { JsonTokenType.True => true, JsonTokenType.False => false, JsonTokenType.String when bool.TryParse(reader.GetString(), out var value) => value, JsonTokenType.String when int.TryParse(reader.GetString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var number) => number != 0, JsonTokenType.Number when reader.TryGetInt64(out var number) => number != 0, _ => throw new JsonException($"Can not convert JSON token {reader.TokenType} to Boolean.") }; } public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) { writer.WriteBooleanValue(value); } } private sealed class FlexibleStringConverter : JsonConverter { public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return reader.TokenType switch { JsonTokenType.Null => null, JsonTokenType.String => reader.GetString(), JsonTokenType.Number => _ReadRawJsonValue(ref reader), JsonTokenType.True => bool.TrueString, JsonTokenType.False => bool.FalseString, _ => _ReadRawJsonValue(ref reader) }; } private static string _ReadRawJsonValue(ref Utf8JsonReader reader) { using var document = JsonDocument.ParseValue(ref reader); return document.RootElement.GetRawText(); } public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options) { writer.WriteStringValue(value); } } }