Punti chiave per progettare contratti di dati API JSON
In sintesi: Sei cose sono molto difficili da cambiare una volta rilasciate: envelope delle risposte, formati degli errori, forma della paginazione, rappresentazione di ID e tempo, semantica di null versus assenza, ed evoluzione delle versioni.

In sintesi: fissa nel contratto l'envelope delle risposte, il formato degli errori, la rappresentazione di ID e tempo e la semantica di null-versus-assenza prima di discutere i dettagli di paginazione e denominazione; avvolgi sempre il livello superiore in un envelope data così paginazione e ID di tracciamento possono essere aggiunti dopo senza rompere i chiamanti esistenti.
Non esistono risposte standard nella progettazione API, ma ci sono alcune decisioni "difficili da ritirare una volta scelte" che è meglio pensare prima di scrivere la prima riga di codice.
1. Avvolgere o no la risposta
Tre stili:
// Style A: bare response
{ "id": 1, "name": "张三" }
// Style B: data envelope
{ "data": { "id": 1, "name": "张三" } }
// Style C: data + metadata
{ "data": { "id": 1, "name": "张三" }, "meta": { "requestId": "a1b2" } }
| Stile | Vantaggi | Svantaggi |
|---|---|---|
| Risposta nuda | Il più semplice, un livello in meno per il frontend | Non può allegare metadati; un array al livello superiore comporta rischi di dirottamento JSON ed evoluzione |
| Envelope data | Facile aggiungere meta / errors / links |
Un livello di annidamento in più, il frontend deve scrivere res.data.data |
| Dati + metadati | Migliore osservabilità | Richiede un accordo rigoroso su quali campi appartengono a meta |
Raccomandazione: usa un envelope data in modo uniforme sia per le liste sia per i dettagli, e non rendere mai il livello superiore un array. Così, aggiungere paginazione o ID di tracciamento in seguito non rompe mai il contratto.
2. Formato degli errori: copia RFC 9457
Inventare la propria struttura di errore è un'enorme fonte di costo di collaborazione. RFC 9457 (che sostituisce RFC 7807) definisce application/problem+json:
{
"type": "https://bbdrug.com/problems/insufficient-balance",
"title": "余额不足",
"status": 422,
"detail": "账户余额 12.00 元,本次扣款需要 30.00 元",
"instance": "/accounts/A1001/payments/p-889",
"balance": "12.00",
"required": "30.00"
}
| Campo | Significato | Obbligatorio |
|---|---|---|
type |
URI stabile del tipo di problema | Raccomandato |
title |
Breve riassunto leggibile dagli umani | Raccomandato |
status |
Codice di stato HTTP | Raccomandato |
detail |
Spiegazione specifica di cosa è successo questa volta | Opzionale |
instance |
L'istanza di risorsa che ha avuto il problema | Opzionale |
| Campi di estensione | Campi di business arbitrari | Opzionale |
I campi di estensione (come balance / required sopra) sono consentiti dalla specifica, e il frontend può usarli per messaggi precisi invece di analizzare il testo di detail.
3. Paginazione: il cursore batte l'offset
// Offset pagination
{ "data": [], "page": { "offset": 40, "limit": 20, "total": 137 } }
// Cursor pagination
{ "data": [], "page": { "nextCursor": "eyJpZCI6MTAwfQ", "hasMore": true } }
| Dimensione | Paginazione offset | Paginazione a cursore |
|---|---|---|
| Difficoltà implementativa | Bassa (LIMIT/OFFSET) |
Media (richiede una chiave di ordinamento stabile) |
| Prestazioni su pagine profonde | Scarse (più grande l'offset, più lento) | Buone |
| Quando i dati cambiano | Duplicati o elementi mancanti | Stabile |
| Salto a una pagina | Possibile | Non possibile |
| Necessita di un conteggio totale | Richiede un conteggio extra | Non richiesto |
Raccomandazione: usa la paginazione offset per le tabelle di amministrazione (necessitano salti di pagina e totali), e la paginazione a cursore per i feed infiniti rivolti all'utente.
4. ID e tempo
4.1 Usa sempre stringhe per gli ID
Il ragionamento è trattato nell'articolo sui tipi: sul lato JavaScript, i valori sopra 2^53 perdono silenziosamente precisione.
Anche se l'ID attuale è un piccolo intero auto-incrementale, usare stringhe lascia spazio per passare in seguito a ID snowflake.
{ "id": "1000000000000000001" }
4.2 Usa stringhe RFC 3339 per il tempo
{
"createdAt": "2026-10-30T10:41:45+08:00",
"updatedAt": "2026-10-30T02:41:45Z"
}
- Non usare numeri timestamp in millisecondi: sono difficili da leggere, e l'unità (secondi/millisecondi) viene spesso confusa;
- Specifica esplicitamente il fuso orario: o con
Zo con un offset; non usare mai "ora locale senza fuso orario"; - Nomina i campi in modo uniforme come
xxxAtcosì è evidente a colpo d'occhio che si tratta di un istante nel tempo.
5. null e assente
Devi sceglierne uno e scriverlo nella documentazione:
| Approccio | Esprimere "nessun valore" | Esprimere "non modificare" |
|---|---|---|
| Approccio A | Il valore del campo è null |
Omettere il campo nella richiesta |
| Approccio B | Il campo non esiste | Omettere il campo nella richiesta |
Il problema dell'Approccio A è che "omesso" e "inviato come null" significano cose diverse in un PATCH, il che è facile da fraintendere;
il problema dell'Approccio B è che il JSON.stringify del frontend elimina i campi il cui valore è undefined, il che per caso corrisponde alla semantica.
Raccomandazione pratica:
- Usa l'Approccio A per i corpi delle risposte (
nullsignifica chiaramente nessun valore, e il frontend non deve distinguereundefined); - Usa l'Approccio B per i corpi delle richieste (specialmente
PATCH), combinato con Merge Patch o JSON Patch per rendere esplicita la semantica.
6. Denominazione e struttura dei campi
| Decisione | Scelte comuni | Avvertenza |
|---|---|---|
| Stile di denominazione | camelCase (ecosistema JS) / snake_case (Python, database) |
Uniforme in tutto il progetto, non mescolare |
| Valori enum | Minuscole con underscore, "pending_review" |
Non usare numeri; sono difficili da leggere |
| Campi booleani | isActive / enabled |
Non usare status: 0/1 |
| Campi array | Nomi plurali, items |
Restituisci [] per gli array vuoti, mai null |
| Profondità di annidamento | Non più di 3 livelli | Oltre, dividi o offri un parametro expand |
Restituire [] invece di null per gli array vuoti conta molto: data.items.map(...) sul frontend non andrà in crash per un null.
7. Strategia di evoluzione
| Modifica | Rompe la compatibilità | Come gestirla |
|---|---|---|
| Aggiungere un campo opzionale | No | Spediscilo direttamente |
| Aggiungere un campo obbligatorio | Sì | Aggiungi un valore default, o rilascia per versione |
| Rimuovere un campo | Sì | Marchialo prima come deprecato, osserva i chiamanti, poi rimuovilo |
| Cambiare il tipo di un campo | Sì | Aggiungi un nuovo nome di campo e scrivi in doppio durante la transizione |
| Cambiare l'insieme dei valori enum | Dipende | Il client deve avere un ripiego per i valori sconosciuti |
| Cambiare la semantica di un campo | Sì | Trattalo come un nuovo campo |
Tre regole di disciplina:
- I client devono essere tolleranti ai campi sconosciuti (non chiuderti con
additionalProperties: false); - I client devono avere un ramo di ripiego per i valori enum sconosciuti;
- Mantieni i campi deprecati per almeno una versione major, e marchiali con
Deprecationnegli header delle risposte o nella documentazione.
8. Un esempio completo
{
"data": [
{
"id": "1001",
"name": "JSON 权威指南",
"price": "89.50",
"tags": ["json", "spec"],
"publishedAt": "2026-09-01T00:00:00+08:00",
"author": null
}
],
"page": { "nextCursor": null, "hasMore": false },
"meta": { "requestId": "req-7f3a", "tookMs": 12 }
}
Questa risposta riflette tutte le convenzioni sopra: envelope data, ID stringa, importi come stringhe, timestamp con fuso orario,
null che significa mancante, array vuoto, paginazione a cursore, e meta per le informazioni di osservabilità.
9. Lista di controllo
- [ ] Il livello superiore non è un array; usa un envelope
datain modo uniforme; - [ ] Le risposte di errore seguono RFC 9457 e includono un
typestabile; - [ ] La forma della paginazione corrisponde allo scenario di business;
- [ ] Gli ID sono stringhe, e il tempo usa RFC 3339;
- [ ] La semantica di
nulle assenza è scritta nella documentazione; - [ ] Lo stile di denominazione è uniforme in tutto il progetto, e gli enum usano stringhe minuscole;
- [ ] Il client ha ripieghi per campi sconosciuti e valori enum sconosciuti;
- [ ] Le modifiche che rompono passano attraverso versioni o una transizione a doppia scrittura.
FAQ
Come dovrei progettare un'API JSON evolvibile?
Avvolgi liste e dettagli in modo uniforme in un envelope data e non rendere mai il livello superiore un array, così paginazione e ID di tracciamento possono essere aggiunti senza rompere il contratto. Usa ID stringa e timestamp RFC 3339, e scrivi nella documentazione la semantica di null-versus-assenza. Mantieni i nuovi campi opzionali e semanticamente indipendenti, e dai ai client ripieghi per campi sconosciuti e valori enum.
Perché non inventare un formato di errore personalizzato?
Inventare la propria struttura di errore è un'enorme fonte di costo di collaborazione. Copia application/problem+json di RFC 9457, identifica i problemi con un type stabile, e consenti campi di estensione come balance e required, così il frontend può costruire messaggi precisi senza analizzare il testo di detail.
Ogni endpoint dovrebbe usare la paginazione a cursore?
No. Le tabelle di amministrazione necessitano salti di pagina e totali, quindi la paginazione offset si adatta meglio; i feed infiniti rivolti all'utente dovrebbero usare la paginazione a cursore, che resta stabile sulle pagine profonde ed evita duplicati o buchi quando i dati cambiano. Scegli in base a se lo scenario necessita salti di pagina e totali.