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>
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
WriteJsonvyhazujeNotImplementedException:IdokladSdk.Serialization.DateTimeConverteru 14 položek typuDateTimeIdokladSdk.Serialization.NotificationJsonConverterpřímo u typuNotificationListGetModel
- 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říkladNullablePropertyJsonConverteruNullableProperty<T>, zůstávají beze změny. - Nahrazuje se konvertor na položce (
Converter), na kolekci (ItemConverter) i na typu (JsonContract.Converter), protožeNotificationJsonConverterje uvedený u typu. - Čtení se nemění,
CanReadiReadJsonse 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í
NamingStrategyz 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:
dotnet build -c Releasebez chyb.- 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
- před opravou selhalo 14 typů na
- Round trip: serializace a zpětná deserializace modelu zachovala všech 7 položek
DateTime. - Čtení payloadu ve tvaru iDokladu (
"DateOfIssue":"2026-08-25T00:00:00") dává stejný výsledek před i po opravě. - Běh služby proti falešnému iDoklad API (
IDOKLAD_API_URLaIDOKLAD_IDENTITY_URL):GET /healthvrátil 200,GET /docs/v1/swagger.json200 aGET /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.