Strutture JSON avanzate: Design patterns e architetture complesse
Guida avanzata alle strutture JSON: design patterns, schema evolution, relazioni tra oggetti, normalizzazione, ottimizzazione, architetture scalabili.
Big JSON Team
• Technical WriterExpert in JSON data manipulation, API development, and web technologies. Passionate about creating tools that make developers' lives easier.
# Strutture JSON avanzate: Design patterns
Progettare strutture JSON efficaci è un'arte. Questa guida ti mostrerà pattern avanzati, best practices e tecniche per creare architetture JSON scalabili e manutenibili.
Design Patterns fondamentali
1. Flat vs Nested
Flat (denormalizzato):{
"ordini": [
{
"id": 1,
"prodotto_id": 101,
"prodotto_nome": "Laptop",
"prodotto_prezzo": 999,
"cliente_id": 501,
"cliente_nome": "Marco Rossi",
"cliente_email": "marco@example.com"
}
]
}
Pros:
- ✅ Query semplici
- ✅ Meno joins
- ✅ Veloce per letture
- ❌ Duplicazione dati
- ❌ Difficile aggiornare
- ❌ File più grandi
{
"ordini": [
{
"id": 1,
"prodotto": {
"id": 101,
"nome": "Laptop",
"prezzo": 999
},
"cliente": {
"id": 501,
"nome": "Marco Rossi",
"email": "marco@example.com"
}
}
]
}
Pros:
- ✅ Nessuna duplicazione
- ✅ Facile aggiornare
- ✅ Struttura logica chiara
- ❌ Query più complesse
- ❌ Più profondo da navigare
- Flat: API responses, read-heavy, performance critica
- Nested: Domain models, write-heavy, relazioni importanti
2. Referenced vs Embedded
Embedded (tutto incluso):{
"autore": {
"id": 1,
"nome": "Marco Rossi",
"libri": [
{
"id": 101,
"titolo": "Il mio primo libro",
"anno": 2020,
"pagine": 350
},
{
"id": 102,
"titolo": "Secondo libro",
"anno": 2022,
"pagine": 420
}
]
}
}
Referenced (con ID):
{
"autori": [
{
"id": 1,
"nome": "Marco Rossi",
"libri_ids": [101, 102]
}
],
"libri": [
{
"id": 101,
"titolo": "Il mio primo libro",
"autore_id": 1,
"anno": 2020
},
{
"id": 102,
"titolo": "Secondo libro",
"autore_id": 1,
"anno": 2022
}
]
}
Regola thumb:
- Embed: Relazione 1-a-1, dati piccoli, sempre caricati insieme
- Reference: Relazione 1-a-molti, dati grandi, non sempre necessari
3. Polymorphic structures
Union types con discriminator:{
"notifiche": [
{
"type": "email",
"id": 1,
"destinatario": "user@example.com",
"oggetto": "Benvenuto",
"corpo": "Grazie per esserti registrato"
},
{
"type": "sms",
"id": 2,
"numero": "+39123456789",
"testo": "Codice: 1234"
},
{
"type": "push",
"id": 3,
"device_id": "abc123",
"titolo": "Nuovo messaggio",
"badge": 5
}
]
}
Pattern:
- Campo
typeidentifica variante - Campi specifici per ogni tipo
- TypeScript discriminated unions
type Notifica =
| { type: 'email'; destinatario: string; oggetto: string; corpo: string }
| { type: 'sms'; numero: string; testo: string }
| { type: 'push'; device_id: string; titolo: string; badge: number };
function inviaNotifica(notifica: Notifica) {
switch (notifica.type) {
case 'email':
return inviaEmail(notifica.destinatario, notifica.oggetto);
case 'sms':
return inviaSMS(notifica.numero, notifica.testo);
case 'push':
return inviaPush(notifica.device_id, notifica.titolo);
}
}
Pattern API Response
Envelope pattern
Wrap response in oggetto standard:{
"success": true,
"data": {
"utenti": [
{"id": 1, "nome": "Marco"},
{"id": 2, "nome": "Laura"}
]
},
"meta": {
"pagination": {
"page": 1,
"perPage": 20,
"total": 100,
"totalPages": 5
},
"timestamp": "2026-01-26T10:00:00Z"
}
}
Errori:
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Email non valida",
"details": {
"field": "email",
"value": "invalid-email",
"constraint": "must be valid email"
}
},
"meta": {
"requestId": "req-abc123",
"timestamp": "2026-01-26T10:00:00Z"
}
}
Vantaggi:
- ✅ Struttura consistente
- ✅ Metadata separati da dati
- ✅ Facile error handling
- ✅ Estendibile
HATEOAS pattern
Hypermedia as the Engine of Application State:{
"ordine": {
"id": 123,
"stato": "in_preparazione",
"totale": 99.99,
"articoli": [...],
"_links": {
"self": {
"href": "/api/ordini/123"
},
"cliente": {
"href": "/api/clienti/456"
},
"annulla": {
"href": "/api/ordini/123/annulla",
"method": "POST"
},
"tracking": {
"href": "/api/ordini/123/tracking"
}
}
}
}
Vantaggi:
- ✅ Self-documenting API
- ✅ Client decoupling
- ✅ Dynamic navigation
Sparse fieldsets
Client sceglie campi da ricevere: Request:GET /api/utenti/123?fields=id,nome,email
Response:
{
"id": 123,
"nome": "Marco Rossi",
"email": "marco@example.com"
}
Invece di tutto:
{
"id": 123,
"nome": "Marco Rossi",
"email": "marco@example.com",
"indirizzo": {...},
"preferenze": {...},
"storico_ordini": [...],
// ... molti altri campi non necessari
}
Implementazione:
function selectFields(obj, fields) {
if (!fields) return obj;
const fieldList = fields.split(',');
const result = {};
for (const field of fieldList) {
if (obj.hasOwnProperty(field)) {
result[field] = obj[field];
}
}
return result;
}
// Express endpoint
app.get('/api/utenti/:id', async (req, res) => {
const utente = await User.findById(req.params.id);
const filtered = selectFields(utente, req.query.fields);
res.json(filtered);
});
Versioning strategies
1. URL versioning
// v1
GET /api/v1/utenti/123
{
"nome": "Marco Rossi",
"email": "marco@example.com"
}
// v2 (campi renamed)
GET /api/v2/utenti/123
{
"fullName": "Marco Rossi",
"emailAddress": "marco@example.com",
"createdAt": "2026-01-20T10:00:00Z"
}
2. Namespace pattern
{
"v1": {
"nome": "Marco",
"età": 30
},
"v2": {
"fullName": "Marco Rossi",
"age": 30,
"birthDate": "1996-01-15"
}
}
3. Schema version field
{
"schemaVersion": "2.0",
"data": {
"fullName": "Marco Rossi",
"emailAddress": "marco@example.com"
}
}
Ottimizzazione strutture
Compressione campi
Prima:{
"products": [
{
"productId": 1,
"productName": "Laptop",
"productPrice": 999,
"productCategory": "electronics"
}
]
}
Dopo (abbreviati):
{
"p": [
{
"i": 1,
"n": "Laptop",
"pr": 999,
"c": "electronics"
}
]
}
Risparmio: ~40% dimensione
Con mapping:
{
"_schema": {
"p": "products",
"i": "id",
"n": "name",
"pr": "price",
"c": "category"
},
"p": [
{"i": 1, "n": "Laptop", "pr": 999, "c": "electronics"}
]
}
Normalizzazione lookup
Prima (ripetitivo):{
"ordini": [
{
"id": 1,
"prodotto": {
"id": 101,
"nome": "Laptop",
"categoria": "Elettronica"
}
},
{
"id": 2,
"prodotto": {
"id": 101,
"nome": "Laptop",
"categoria": "Elettronica"
}
}
]
}
Dopo (normalizzato):
{
"prodotti": {
"101": {
"nome": "Laptop",
"categoria": "Elettronica"
}
},
"ordini": [
{"id": 1, "prodotto_id": "101"},
{"id": 2, "prodotto_id": "101"}
]
}
Lookup veloce:
const ordine = data.ordini[0];
const prodotto = data.prodotti[ordine.prodotto_id];
// O(1) lookup!
Arrays vs Objects per collections
Array (lista):{
"utenti": [
{"id": 1, "nome": "Marco"},
{"id": 2, "nome": "Laura"}
]
}
Object (map):
{
"utenti": {
"1": {"nome": "Marco"},
"2": {"nome": "Laura"}
}
}
Quando usare Object:
- ✅ Lookup per ID frequenti
- ✅ Aggiornamenti in-place
- ✅ Merge facile
- ✅ Ordine importante
- ✅ Filtering/mapping
- ✅ Standard JSON:API
Schema evolution
Backward compatibility
Versione 1:{
"utente": {
"nome": "Marco",
"email": "marco@example.com"
}
}
Versione 2 (add campo):
{
"utente": {
"nome": "Marco",
"email": "marco@example.com",
"telefono": "+39123456789" // Nuovo, opzionale
}
}
✅ Backward compatible - Client v1 funziona ancora
❌ Breaking change:
{
"utente": {
"fullName": "Marco Rossi", // Renamed!
"emailAddress": "marco@example.com" // Renamed!
}
}
Migration strategies
Dual write:{
"utente": {
// Old format (deprecated)
"nome": "Marco",
// New format
"fullName": "Marco Rossi",
"_deprecated": {
"nome": "Use fullName instead"
}
}
}
Transformation layer:
function transformV1toV2(v1Data) {
return {
fullName: v1Data.nome,
emailAddress: v1Data.email,
phoneNumber: v1Data.telefono || null
};
}
// API endpoint
app.get('/api/utenti/:id', async (req, res) => {
const utente = await User.findById(req.params.id);
// Check version in header/query
const version = req.get('API-Version') || '1';
if (version === '2') {
res.json(transformV1toV2(utente));
} else {
res.json(utente);
}
});
Advanced patterns
Event sourcing
Eventi invece di stato:{
"eventi": [
{
"type": "UserCreated",
"timestamp": "2026-01-20T10:00:00Z",
"data": {
"userId": 123,
"email": "marco@example.com"
}
},
{
"type": "EmailUpdated",
"timestamp": "2026-01-22T14:30:00Z",
"data": {
"userId": 123,
"oldEmail": "marco@example.com",
"newEmail": "marco.rossi@example.com"
}
},
{
"type": "UserDeleted",
"timestamp": "2026-01-25T09:00:00Z",
"data": {
"userId": 123,
"reason": "user_request"
}
}
]
}
Rebuild state:
function rebuildState(events) {
const state = {};
for (const event of events) {
switch (event.type) {
case 'UserCreated':
state[event.data.userId] = {
email: event.data.email,
createdAt: event.timestamp
};
break;
case 'EmailUpdated':
state[event.data.userId].email = event.data.newEmail;
break;
case 'UserDeleted':
delete state[event.data.userId];
break;
}
}
return state;
}
JSON Patch (RFC 6902)
Partial updates:[
{
"op": "replace",
"path": "/email",
"value": "new@example.com"
},
{
"op": "add",
"path": "/telefono",
"value": "+39123456789"
},
{
"op": "remove",
"path": "/vecchio_campo"
}
]
Applicazione:
const jsonpatch = require('fast-json-patch');
// Document originale
const doc = {
nome: "Marco",
email: "marco@example.com"
};
// Patch
const patch = [
{ op: "replace", path: "/email", value: "new@example.com" }
];
// Applica
const newDoc = jsonpatch.applyPatch(doc, patch).newDocument;
Graph structures
Tree:{
"id": 1,
"nome": "Root",
"children": [
{
"id": 2,
"nome": "Child 1",
"children": [
{
"id": 3,
"nome": "Grandchild 1",
"children": []
}
]
},
{
"id": 4,
"nome": "Child 2",
"children": []
}
]
}
Adjacency list:
{
"nodes": [
{"id": 1, "nome": "Root", "parent_id": null},
{"id": 2, "nome": "Child 1", "parent_id": 1},
{"id": 3, "nome": "Grandchild 1", "parent_id": 2},
{"id": 4, "nome": "Child 2", "parent_id": 1}
]
}
Più efficiente per query e aggiornamenti!
Best practices finali
1. Consistenza naming
CamelCase:{
"userId": 123,
"firstName": "Marco",
"emailAddress": "marco@example.com"
}
snake_case:
{
"user_id": 123,
"first_name": "Marco",
"email_address": "marco@example.com"
}
Scegli uno e resta consistente!
2. Usa null vs omit
Con null:{
"nome": "Marco",
"telefono": null
}
Omesso:
{
"nome": "Marco"
}
Regola: Usa null quando il campo esiste ma è vuoto, ometti quando il campo non è applicabile
3. Date/Time format
ISO 8601 sempre:{
"createdAt": "2026-01-26T10:00:00Z",
"updatedAt": "2026-01-26T14:30:00+01:00",
"birthDate": "1996-01-15"
}
4. Pagination standard
{
"data": [...],
"pagination": {
"page": 1,
"perPage": 20,
"total": 1000,
"totalPages": 50,
"hasNext": true,
"hasPrevious": false,
"links": {
"first": "/api/items?page=1",
"prev": null,
"next": "/api/items?page=2",
"last": "/api/items?page=50"
}
}
}
Conclusione
Key takeaways:✅ Design per il use case:
- Read-heavy → Flat, embedded
- Write-heavy → Normalized, referenced
- Flexible → Polymorphic con discriminator
✅ Pensa a evoluzione:
- Versioning strategy dall'inizio
- Backward compatibility
- Migration paths
✅ Ottimizza con criterio:
- Profila prima di ottimizzare
- Compressione solo se necessaria
- Normalizzazione per dati ripetitivi
✅ Consistenza sopra tutto:
- Naming conventions
- Error formats
- Response envelopes
Strutture JSON ben progettate rendono API manutenibili, scalabili e piacevoli da usare!
Articoli Correlati
Cos'è JSON? Guida completa per principianti
Scopri cos'è JSON, la sua sintassi, i tipi di dati e i casi d'uso. Una guida completa e adatta ai principianti per comprendere JavaScript Object Notation.
Comprendere JSON Schema: Validazione e documentazione dati
Guida completa a JSON Schema: validazione dati, tipi, vincoli, pattern, esempi pratici e librerie. Impara a validare e documentare API JSON professionalmente.
Lavorare con file JSON grandi: Streaming, performance, best practices
Guida completa per gestire file JSON di grandi dimensioni: streaming, chunk processing, memory optimization, strumenti e tecniche per big data JSON.