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>
97 lines
4.8 KiB
Markdown
97 lines
4.8 KiB
Markdown
# 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:
|
|
|
|
```text
|
|
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é:
|
|
|
|
```text
|
|
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:
|
|
|
|
```text
|
|
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](datumy-utc.md).
|