Files
idoklad/documentation/serializace-odpovedi.md
T
JiriUhlirandClaude Opus 5 73f01d0228 oprava datumu: prijem data bez zony jako UTC
SDK validuje Post/Patch modely pred odeslanim a u kazde polozky DateTime
vyzaduje Kind == Utc. Datum bez zony, napriklad "2026-08-25", nacetl
Newtonsoft jako Unspecified, takze POST /issued-invoices koncil chybou
"DateTime must be in UTC format" jeste pred volanim iDokladu.

- DateTimeZoneHandling.Utc: hodnota bez zony dostane Kind Utc bez posunu,
  hodnota s offsetem se prepocita do UTC
- ExceptionHandlingMiddleware zachytava IdokladValidationException a vraci
  400 se seznamem vadnych vlastnosti misto prazdne 500

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:51:45 +02:00

4.8 KiB

Serializace odpovědí (JSON) a konvertory SDK

Problém

Endpointy vracející některé modely z IdokladSdk končily chybou 500 a v logu byla výjimka:

System.NotImplementedException: The method or operation is not implemented.
   at IdokladSdk.Serialization.DateTimeConverter.WriteJson(JsonWriter writer, Object value, JsonSerializer serializer)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeConvertable(...)

Příčiny:

  • Část modelů SDK má konvertor přiřazený atributem [JsonConverter(...)].
  • Dva z těchto konvertorů umí jen čtení (deserializaci odpovědí z iDokladu), jejich WriteJson vyhazuje NotImplementedException:
    • IdokladSdk.Serialization.DateTimeConverter u 14 položek typu DateTime
    • IdokladSdk.Serialization.NotificationJsonConverter přímo u typu NotificationListGetModel
  • Služba používá Newtonsoft.Json i pro výstup (AddNewtonsoftJson), takže při serializaci odpovědi Newtonsoft konvertor z atributu použil a request spadl.

Konvertor uvedený atributem má přednost před konvertory registrovanými v SerializerSettings, takže pouhé přidání vlastního konvertoru do nastavení problém neřeší. Vyměnit se musí na úrovni kontraktu, tedy v IContractResolver.

Postižené bylo 14 modelů SDK, mimo jiné:

ReceivedInvoiceGetModel, ReceivedInvoiceListGetModel, BankAccountGetModel,
BankAccountListGetModel, CashRegisterGetModel, CashRegisterListGetModel,
CreditNoteGetModel, CreditNoteListGetModel, IssuedInvoiceCopyGetModel,
ProformaInvoiceCopyGetModel, RecurringSettingGetModel, RecurringSettingListGetModel,
SubscriptionGetModel, NotificationListGetModel

Prakticky to znamená, že padaly například GET /received-invoices, GET /registers/bank-accounts a GET /integration/notifications.

Řešení

Soubor Účel
Infrastructure/SdkWriteBypassJsonConverter.cs Obal nad konvertorem SDK. Čtení deleguje na něj, CanWrite je false, takže zápis provede standardní serializace po položkách.
Infrastructure/SdkContractResolver.cs Potomek DefaultContractResolver, který tímto obalem nahradí konvertory SDK neumějící zápis.
Program.cs Registrace resolveru v AddNewtonsoftJson.

Detaily:

  • Nahrazují se jen konvertory ze jmenného seznamu (DateTimeConverter, NotificationJsonConverter). Konvertory SDK, které zápis umí, například NullablePropertyJsonConverter u NullableProperty<T>, zůstávají beze změny.
  • Nahrazuje se konvertor na položce (Converter), na kolekci (ItemConverter) i na typu (JsonContract.Converter), protože NotificationJsonConverter je uvedený u typu.
  • Čtení se nemění, CanRead i ReadJson se delegují na původní konvertor SDK. Deserializace requestů je tedy stejná jako předtím.
  • Zápis datumů se neformátuje vlastním způsobem. Jde stejnou cestou jako u položek DateTime, které konvertor nikdy neměly, takže je v odpovědi jednotný tvar.
  • Původní NamingStrategy z výchozího resolveru MVC se přenese, aby se nezměnily názvy vlastností v JSON odpovědích.

Ověření

Provedeno před nasazením:

  1. dotnet build -c Release bez chyb.
  2. Serializace všech 536 veřejných typů SDK s bezparametrickým konstruktorem, se stejným nastavením, jaké dostane AddNewtonsoftJson:
    • před opravou selhalo 14 typů na NotImplementedException
    • po opravě 0 typů
    • 521 typů serializovatelných v obou případech dalo identický JSON, názvy vlastností se tedy nezměnily
  3. Round trip: serializace a zpětná deserializace modelu zachovala všech 7 položek DateTime.
  4. Čtení payloadu ve tvaru iDokladu ("DateOfIssue":"2026-08-25T00:00:00") dává stejný výsledek před i po opravě.
  5. Běh služby proti falešnému iDoklad API (IDOKLAD_API_URL a IDOKLAD_IDENTITY_URL): GET /health vrátil 200, GET /docs/v1/swagger.json 200 a GET /received-invoices, který dříve padal, vrátil 200 s vyplněnými datumy.

Po nasazení stačí zavolat:

GET https://services.csbot.cz/apps/idoklad/received-invoices?page=1&pageSize=1

Očekávaný výsledek je HTTP 200 a v logu žádná NotImplementedException.

Poznámka mimo rozsah opravy

Pokud iDoklad vrátí odpověď, kterou SDK neumí zpracovat, vyhodí IdokladValidationException. Ta není potomkem IdokladBaseException, takže ji ExceptionHandlingMiddleware nezachytil a request skončil jako neošetřená 500 místo 502. Zjištěno při testu proti falešnému API, tehdy neopravováno, protože to nesouvisí se serializací odpovědí.

Doplněno později: tato výjimka se ve skutečnosti vyhazuje hlavně při validaci Post/Patch modelu před odesláním a způsobovala prázdné 500. Middleware ji nyní zachytává a vrací 400, viz datumy-utc.md.