diff --git a/src/OutlookAddin.Core/Json/FlexibleDateTimeOffsetConverter.cs b/src/OutlookAddin.Core/Json/FlexibleDateTimeOffsetConverter.cs
new file mode 100644
index 0000000..03cad1b
--- /dev/null
+++ b/src/OutlookAddin.Core/Json/FlexibleDateTimeOffsetConverter.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Globalization;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MarcusLaw.OutlookAddin.Core.Json
+{
+ ///
+ /// EspoCRM serializes date/datetime fields in one of these shapes:
+ /// * "YYYY-MM-DD HH:mm:ss" (datetime, naive — server is UTC by convention)
+ /// * "YYYY-MM-DD" (date only)
+ /// * ISO 8601 "YYYY-MM-DDTHH:mm:ss(+zz:zz|Z)" (some custom builds)
+ /// * empty string / null
+ /// .NET's default Utf8JsonReader.GetDateTimeOffset only accepts strict
+ /// ISO 8601 with a timezone, so any of the first two forms throws and
+ /// the whole list response fails to deserialize. This converter parses
+ /// all four and assumes UTC when no offset is present.
+ ///
+ public sealed class FlexibleDateTimeOffsetConverter : JsonConverter
+ {
+ private static readonly string[] Formats = new[]
+ {
+ "yyyy-MM-dd HH:mm:ss",
+ "yyyy-MM-dd HH:mm",
+ "yyyy-MM-ddTHH:mm:ss",
+ "yyyy-MM-ddTHH:mm:ssK",
+ "yyyy-MM-ddTHH:mm:ss.fffK",
+ "yyyy-MM-dd"
+ };
+
+ public override DateTimeOffset? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ switch (reader.TokenType)
+ {
+ case JsonTokenType.Null:
+ return null;
+ case JsonTokenType.String:
+ var s = reader.GetString();
+ if (string.IsNullOrWhiteSpace(s)) return null;
+ // Fast path — ISO with offset, what .NET accepts natively.
+ if (DateTimeOffset.TryParse(
+ s, CultureInfo.InvariantCulture,
+ DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
+ out var dto))
+ {
+ return dto;
+ }
+ foreach (var fmt in Formats)
+ {
+ if (DateTime.TryParseExact(
+ s, fmt, CultureInfo.InvariantCulture,
+ DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
+ out var dt))
+ {
+ return new DateTimeOffset(dt, TimeSpan.Zero);
+ }
+ }
+ return null;
+ case JsonTokenType.Number:
+ // Some installs emit Unix epoch seconds.
+ if (reader.TryGetInt64(out var epoch))
+ {
+ return DateTimeOffset.FromUnixTimeSeconds(epoch);
+ }
+ return null;
+ default:
+ reader.Skip();
+ return null;
+ }
+ }
+
+ public override void Write(Utf8JsonWriter writer, DateTimeOffset? value, JsonSerializerOptions options)
+ {
+ if (value == null) writer.WriteNullValue();
+ else writer.WriteStringValue(value.Value.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture));
+ }
+ }
+}
diff --git a/src/OutlookAddin.Core/Services/EspoCrmClient.cs b/src/OutlookAddin.Core/Services/EspoCrmClient.cs
index 3be29d0..96252ed 100644
--- a/src/OutlookAddin.Core/Services/EspoCrmClient.cs
+++ b/src/OutlookAddin.Core/Services/EspoCrmClient.cs
@@ -20,12 +20,23 @@ namespace MarcusLaw.OutlookAddin.Core.Services
{
private const string ApiPrefix = "api/v1/";
- private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
+ private static readonly JsonSerializerOptions JsonOptions = CreateJsonOptions();
+
+ private static JsonSerializerOptions CreateJsonOptions()
{
- PropertyNameCaseInsensitive = true,
- DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
- Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
- };
+ var options = new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true,
+ DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
+ Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
+ };
+ // EspoCRM emits dates as "YYYY-MM-DD HH:mm:ss" (no timezone) which
+ // .NET's built-in DateTimeOffset reader rejects. Register a global
+ // tolerant converter so every model that exposes a DateTimeOffset?
+ // works without per-property attributes.
+ options.Converters.Add(new MarcusLaw.OutlookAddin.Core.Json.FlexibleDateTimeOffsetConverter());
+ return options;
+ }
private readonly HttpClient _httpClient;
private readonly ILogger _logger;