Escriu per cercar…

JSON

JSON és un format de text que permet representar qualsevol estructura de dades.

Introducció

JSON es basa en una propietat fonamental dels llenguatges de programació: qualsevol estructura de dades es pot representar amb diccionaris i llistes de forma recursiva.

JSON permet convertir qualsevol estructura de dades a una representació de text simple.

D’aquesta manera, tant tu com qualsevol llenguatge de programació podeu llegir i escriure dades JSON.

Diccionari

El concepte de diccionari ve del diccionari físic, i és el terme que utilitza Python encara que en altres llenguatges té un altre nom.

A continuació tens un exemple molt senzill d’una representació JSON:

json
{ "name": "Eva" }

Es tracta d’un diccionari amb només una entrada, on "name" és la clau i "Eva" és el valor.

El nom és un string i ha d’anar envoltat de cometes dobles ""

TypeScript

TypeScript no inclou un tipus de dades natiu “diccionari” perquè els objectes en TypeScript es poden utilitzar com si fossin diccionaris si el tipus és any.

Per exemple, aquí tens un objecte JSON que representa una persona:

json
{
  "name": "Eva",
  "age": 30
}

A continuació tens el mateix objecte representat com un objecte de TypeScript:

ts
{
    name: "Eva",
    age: 30
}

Cap diferència? 🤗

Fixa’t que l’única diferència és que JSON utilitza cometes dobles per a les claus!

I sempre has d’utilitzar cometes dobles per representar un string, mai pots utilitzar cometes simples: ‘Eva’ no és vàlid en JSON.

Instal·la Deno:

Crea un projecte nou amb el nom json:

ps
mkdir json
cd json
deno init
Note
  • Pots obrir el projecte amb PyCharm.
  • Instal·la el plugin de Deno

JSON ens permet convertir una estructura a un string amb la funció JSON.stringify().

Crea un fitxer write.ts.

ts
const eva = {
    name: "Eva",
    age: 30
}

const json = JSON.stringify(eva)

console.log(json)

Pots veure que per pantalla s’imprimeix un string: name i age van entre cometes dobles (""):

ps
deno run write.ts
{"name":"Eva","age":30}

Un cop tenim el JSON convertit en string, el podem escriure en diversos destins, com una resposta d’API, un fitxer o qualsevol altra cosa.

Per exemple, escriu el string al fitxer eva.json:

ts
const eva = {
  name: "Eva",
  age: 30,
};

const json = JSON.stringify(eva, null, 2);

Deno.mkdirSync("data", {recursive: true});
Deno.writeTextFileSync("data/eva.json", json);
Pretty Printing

Als exemples anteriors utilitzem JSON.stringify(eva, null, 2). El 2 indica a Deno que utilitzi dos espais d’indentació, cosa que fa que el fitxer .json resultant sigui molt més fàcil de llegir per a les persones!

Note

Com que estem escrivint un script senzill on el bloqueig no és cap problema, pots utilitzar la versió síncrona.

Note

En Deno, els mètodes mkdir i mkdirSync inclouen una opció recursiva. Quan val true, es comporta com mkdir -p: crea el directori si no existeix (incloent-hi els directoris pare) i no llança cap error si el directori ja existeix.

Deno és segur per defecte. Per permetre que l’script escrigui al disc, l’has d’executar amb el flag --allow-write al terminal:

ps
deno run --allow-write .\write.ts

Si executes l’script pots veure que es crea un fitxer eva.json que tant tu com qualsevol llenguatge de programació podeu llegir sense problemes:

ps
gc data/eva.json
json
{
  "name": "Eva",
  "age": 30
}

Python

Aquí tens l’objecte JSON que representa una persona:

json
{
  "name": "Eva",
  "age": 30
}

A continuació tens el mateix objecte representat com un diccionari en Python:

python
{
  "name": "Eva",
  "age": 30
}

Cap diferència? 🤗

Inicia un projecte Python amb Uv dins de la carpeta del teu projecte:

ps
uv init

Crea l’script read.py:

python
import json

eva_str = '{"name": "Eva", "age": 30}'

eva = json.loads(eva_str)

print(eva)

Amb la funció loads pots llegir un str i convertir-lo en un diccionari Python.

ps
uv run read.py
python
{'name': 'Eva', 'age': 30}

Modifica el fitxer read.py per llegir el fitxer eva.json que ha creat l’script de TypeScript:

python
import json

with open('data/eva.json') as f:
    eva = json.load(f)

print(eva["name"])

Pots veure que TypeScript i Python poden compartir dades 💞:

ps
python.exe .\read.py
Eva

El problema és que poden compartir dades amb qualsevol llenguatge que passi per allà, fins i tot amb tu; no són un bon exemple de fidelitat, JSON és molt, molt simple 😒

Activitats

Task

Aquí tens un objecte TypeScript que representa un llibre:

ts
{
title: "The Art of Computer Programming",
author: "Donald Knuth"
}

Modifica:

  • L’script write.ts perquè escrigui el llibre a book.json
  • L’script read.py perquè llegeixi el llibre i imprimeixi el títol.
Show solution
ts
write.ts
const book = {
    title: "The Art of Computer Programming",
    author: "Donald Knuth"
}

const json = JSON.stringify(book)

Deno.writeTextFileSync("data/book.json", json)
python
read.py
import json

with open('data/book.json') as f:
book = json.load(f)

print(book["title"])

Funciona ! 😀

ps
deno run --allow-write write.ts
uv run read.py
Donald Knuth
Task

Llegeix el fitxer deno.json amb Python i imprimeix per consola els imports de Deno:

Show solution
python
read.py
import json

with open('deno.json') as f:
deno = json.load(f)

print(deno["imports"])

Llista

Una llista és un conjunt ordenat d’elements al qual es pot accedir per posició.

A continuació tens un exemple d’una llista:

json
[ 1, "hello", false ]

Python

Aquí tens una llista en JSON:

json
[1 ,"hello", false]

I la mateixa llista en Python 🤗 :

python
[1 ,"hello", False]

Crea l’script write.py:

python
import json

items = [1 ,"hello", False]

with open('data/list.json', 'w') as file:
    json.dump(items, file)

Amb la funció json.dump converteixes un objecte Python en un str JSON.

ps
uv run write.py

TypeScript

Una llista “json” es representa amb un array de TypeScript: 🤗

ts
[1 ,"hello", False]

Crea el fitxer read.ts per llegir les dades del fitxer list.json:

ts
const text = Deno.readTextFileSync("data/list.json")
const items = JSON.parse(text)

console.log(items)
ps
uv run write.py
deno run --allow-read .\read.ts
[ 1, "hello", false ]

Activitat

Modifica el fitxer write.py:

python
import json

customers = [
    {"name": "Eva", "age": 43},
    {"name": "Esther", "age": 51},
    {"name": "David", "age": 52}
]

with open('data/customers.json', 'w') as file:
    json.dump(customers, file, indent=2)

Executa l’script per crear el fitxer data.json:

ps
uv run write.py

Modifica el fitxer read.ts perquè importi les dades de data.json i imprimeixi només els noms dels clients:

Show solution
ts

Tipus

Tipus bàsics

JSON només té tres tipus bàsics: string, number i boolean!

string

json
["fred", "fred\t", "\b", "", "\t", "\u004A" ]

Un string té aquestes propietats:

  • Un string consisteix en zero o més caràcters Unicode entre cometes dobles ("").
  • Un string entre cometes simples (') no és vàlid – a diferència de TypeScript i Python.

A més, un string pot contenir els caràcters “backslash-escaped” següents:

\"Cometa doble
\\Barra invertida
\/Barra
\bRetrocés
\fSalt de pàgina
\nSalt de línia
\rRetorn de carro
\tTabulador
\uSeguit de quatre dígits hexadecimals

number

json
{
  "age": 29,
  "cost": 299.99,
  "temperature": -10.5,
  "unitCost": 0.2,
  "speedOfLight": 1.23e11,
  "speedOfLight2": 1.23e+11,
  "avogadro": 6.023E23,
  "avogadro2": 6.023E+23,
  "oneHundredth": 10e-3,
  "oneTenth": 10E-2
}

Els nombres segueixen el format de coma flotant de doble precisió de TypeScript i tenen les propietats següents:

  • Els nombres sempre estan en base 10 (només es permeten els dígits del 0 al 9) sense zeros inicials.
  • Els nombres poden tenir una part fraccionària que comença amb un punt decimal (.).
  • Els nombres poden tenir un exponent de 10, que es representa amb la notació e o E amb un signe positiu o negatiu per indicar una exponenciació positiva o negativa.
  • No s’admeten els formats octal i hexadecimal.
  • A diferència de TypeScript, els nombres no poden tenir un valor de NaN (no és un nombre, per a nombres no vàlids) o Infinity.

boolean

json
{
  "isRegistered": true,
  "emailValidated": false
}

Els booleans tenen les propietats següents:

  • Els booleans només poden tenir un valor de true o false.
  • El valor true o false a la dreta dels dos punts (:) no va envoltat de cometes.

null

Encara que tècnicament no sigui un tipus de valor, null és un valor especial en JSON.

json
{
  "address": {
    "line1": "555 Any Street",
    "line2": null,
    "city": "Denver",
    "stateOrProvince": "CO",
    "zipOrPostalCode": "80202",
    "country": "USA"
  }
}

Els valors nuls tenen les característiques següents:

  • No van envoltats de cometes
  • Indiquen que una clau/propietat no té valor
  • Actuen com a marcador de posició

“Tipus” derivats

JSON només té tres tipus bàsics perquè amb un string pots representar qualsevol tipus de dades.

Només cal que tots ens posem d’acord en com representem una dada.

En aquest enllaç tens la guia d’estil de Google respecte a JSON: JSON Style Guide.

Data

Per exemple, Google prefereix que les dates segueixin el format RFC 3339:

json
{
  "date": "2014-03-01T23:46:11-05:00"
}

La data anterior proporciona un desplaçament respecte a l’hora universal coordinada (UTC) (des d’UTC/GMT — Hora mitjana de Greenwich) de -5 hores, que és l’hora estàndard de l’est dels EUA. Tingues en compte que l’RFC 3339 és un perfil de la ISO 8601.

ts
const date = new Date()

// Format RFC 3339
console.log(date.toISOString())  // 2025-02-17T22:20:57.141Z

La diferència principal és que la ISO 8601 de l’Organització Internacional d’Estandardització permet substituir la T (que separa la data i l’hora) per un espai, i l’RFC 3339 no ho permet.

Valors de latitud/longitud

Les API geogràfiques (p. ex., Google Maps) i les API relacionades amb un sistema d’informació geogràfica (SIG) utilitzen dades de latitud/longitud. Per garantir la coherència, Google recomana que les dades de latitud/longitud segueixin l’estàndard ISO 6709.

Segons Google Maps, les coordenades de l’Empire State Building a la ciutat de Nova York són 40,748747° N, 73,985547° O i es representarien en JSON..

json
{
    "empireStateBuilding": "40.748747-73.985547"
}

Aquest exemple segueix el format ±DD.DDDD±DDD.DDDD, amb les convencions següents:

  • La latitud va primer.
  • La latitud nord (de l’equador) és positiva.
  • La longitud est (del primer meridià) és positiva.
  • La latitud/longitud es representa amb un string. No pot ser un nombre a causa del signe menys.

Estructures complexes

Combinant diccionaris i llistes, i amb només tres tipus bàsics, pots representar qualsevol tipus de dades:

json
[
  { "id": 11, "name": "Eva"},
  { "id": 23, "name": "Marc"},
  { "id": 36, "name": "Esther", "address":
    { "street": "Avda. Diagonal", "city": "Barcelona"}
  }
]

A continuació tens el diagrama de classes corresponent:

Referències repetides

Quan transformem una estructura de dades en un text JSON, el procés no codifica referències, sinó que va codificant els objectes que troba al seu pas.

Per exemple, en un projecte l’owner i el mantainer són del tipus User:

A continuació tens un exemple en què, al projecte “Quantum System”, owner i mantainer fan referència al mateix objecte de tipus User:

ts
project.ts
type Project = { name: string; owner: User; mantainer: User };
type User = { name: string };

const david: User = { name: "David" };
const project: Project = {
  name: "Quantum System",
  owner: david,
  mantainer: david,
};

console.log(JSON.stringify(project, null, 2));

Si executes el codi, pots veure que l’owner i el mantainer es codifiquen com si fossin dos objectes diferents.

ps
deno run project.ts
{"name":"Quantum System","owner":{"name":"David"},"mantainer":{"name":"David"}}

Això significa que hi pot haver dades repetides perquè un objecte pot fer referència al mateix objecte de forma directa o indirecta.

Al nostre exemple, si desem el projecte en un fitxer i el tornem a llegir, tindrem un projecte en què l’owner i el mantainer són dos objectes diferents encara que tinguin el mateix nom:

ts
type Project = { name: string; owner: User; mantainer: User };
type User = { name: string };

const david: User = { name: "David" };
let project: Project = {
  name: "Quantum System",
  owner: david,
  mantainer: david,
};

Deno.writeTextFileSync("data/project.json", JSON.stringify(project));

project = JSON.parse(Deno.readTextFileSync("data/project.json"));

console.log(project.owner == project.mantainer);

project.owner.name = "Eva";
console.log(project.mantainer.name); // Eva

Si executes l’script, pots veure que si canvio el nom de l’owner, el nom del mantainer no canvia perquè són dos objectes diferents:

ps
deno run -A project.ts
false
David

Has d’evitar un codi d’aquest tipus si vols compartir aquestes dades amb JSON 💩 !

No hi ha referències, hi ha redundància!

Note

Deno també ofereix un flag --allow-all que concedeix tots els permisos a l’script. Això desactiva completament el sandbox de seguretat, i s’ha d’utilitzar amb precaució.

ts
deno run -A script.ts
deno run --allow-all script.ts

Referències circulars

Com ja s’ha explicat abans, la serialització no té en compte si un objecte ja s’havia serialitzat.

Això significa que, a més de la redundància, la serialització només funciona amb arbres: un objecte arrel, ramificacions i tot acaba en fulles.

Es comença amb un objecte i es van processant de forma recursiva totes les propietats que són referències fins que no queda cap objecte referenciat per processar.

Per exemple, si David té una referència directa o indirecta amb Esther, i Esther té una referència directa o indirecta amb David

la recursió no acabarà mai 💫💫💫💫💫 …

ts
type Person = { name: string; couple: Person | null };

const david: Person = { name: "David", couple: null };
const esther: Person = { name: "Esther", couple: david };
david.couple = esther;

Deno.writeTextFileSync("data/david.json", JSON.stringify(david));

però JSON no ho permet 😱 … no creu en l’amor etern 💞 …

ps
deno run -A couple.ts
error: Uncaught (in promise) TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
|     property 'couple' -> object with constructor 'Object'
--- property 'couple' closes the circle
Deno.writeTextFileSync("data/david.json", JSON.stringify(david));
Note

Deno t’informa exactament del problema Converting circular structure to JSON i d’on es troba la referència circular property ‘couple’ closes the circle.

què pot fer el pobre gatet 🙀🙀 ??

Canviar el disseny 😼 … i no posar data de finalització (sempre optimista, pensar que les parelles són per sempre 💞) …

I ja podem escriure el codi:

ts
type Person = { name: string };
type Couple = { start: string; one: Person; two: Person };

const david: Person = { name: "David" };
const esther: Person = { name: "Esther" };
const couple: Couple = {
  start: "Un noche de lluvía con luna en el cielo",
  one: david,
  two: esther,
};

Deno.writeTextFileSync("data/couple.json", JSON.stringify(couple, null, 2));

I JSON diu que sí 😀, a més és més romàntic 🌹:

ps
deno run -A couple.ts
gc data/couple.json
json
{
  "start": "Un noche de lluvía con luna en el cielo",
  "one": {
    "name": "David"
  },
  "two": {
    "name": "Esther"
  }
}

Que la data hagi d’estar en format RFC 3339 és un estil de Google … el start d’aquest codi també és una data “tipus” string 🌈.

Però hi ha un problema: una parella és cosa de dos, one i two, que no és el mateix que first i second encara que alguns ho interpretaran així, i el he i she fa temps que no s’admet com a exclusiu …

La propera vegada dissenyem un zoo 😒: 🐯 🦒 🐮 🐰 🦓 🐼 🐨 🐑 🦛 🦏 🦬 🐆

Activitat - Vuelos Pájaro Bobo

L’empresa “Vuelos Pájaro Bobo” és una start-up que necessita un sistema bàsic de gestió de vols.

A continuació tens el disseny de l’estructura de dades:

De moment només té vols des de “Barcelona”, codi BCN, i sí, els passatgers s’identifiquen pel nom.

Per tant, si en un vol ja hi ha una passatgera que es diu “Eva” i una altra “Eva” vol viatjar, o es canvia el nom o no pot viatjar en aquest vol 👏.

A continuació tens un exemple per al vol bcn-cdg-tomorrow-morning-after-breakfast.json:

json
{
    "id": "bcn-cdg-tomorrow-morning-after-breakfast",
    "ufoSeen": false,
    "plane": {
        "name": "fearless-ostrich",
        "seats": 50
    },
    "departure": {
        "code": "BCN",
        "name": "Aeroport Josep Tarradellas Barcelona",
        "city": "Barcelona",
        "country": "Spain"
    },
    "arrival": {
        "code": "CDG",
        "name": "Paris-Charles de Gaulle",
        "city": "Paris",
        "country": "France"
    },
    "passengers": [
        {"name": "Julia", "face": "🤗" },
        {"name": "Mary", "face": "🫣" },
        {"name": "Josephine", "face": "🙄" },
        {"name": "Albert", "face": "🤢" },
        {"name": "Mike", "face": "😵‍💫" }
    ]
}

Has de completar l’script bobo.ts perquè permeti afegir passatgers:

ts
console.log("Welcome to Vuelos Pájaro Bobo")

// 👉 el teu torn! 

En acabar has de crear un binari executable:

ps
deno compile bobo.ts

Aquest binari es pot executar directament en qualsevol sistema Windows:

ps
.\bobo.exe
Welcome to Vuelos Pájaro Bobo

Activitat

Fes els mateixos exercicis en Python