← Torna al Blog

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 Team16 min di letturaadvanced
B

Big JSON Team

Technical Writer

Expert in JSON data manipulation, API development, and web technologies. Passionate about creating tools that make developers' lives easier.

16 min di lettura

# 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

Cons:
  • ❌ Duplicazione dati
  • ❌ Difficile aggiornare
  • ❌ File più grandi

Nested (normalizzato):
{

"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

Cons:
  • ❌ Query più complesse
  • ❌ Più profondo da navigare

Quando usare:
  • 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 type identifica variante
  • Campi specifici per ogni tipo
  • TypeScript discriminated unions

TypeScript:
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

Quando usare Array:
  • ✅ 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!

Share:

Articoli Correlati

Read in English