JSON es un formato de texto que permite representar cualquier estructura de datos.

Introducción

JSON se basa en una propiedad fundamental de los lenguajes de programación: cualquier estructura de datos se puede representar mediante diccionarios y listas de forma recursiva.

JSON permite convertir cualquier estructura de datos a una representación de texto simple.

De esta forma, tanto tu como cualquier lenguaje de programación puede leer y escribir datos JSON.

Diccionario

El concepto de diccionario viene del diccionario físico, y es el término que utiliza Python aunque en otros lenguajes tiene otro nombre.

A continuación tienes un ejemplo muy sencillo de una representación JSON:

{ "name": "Eva" }

Se trata de un diccionario con sólo una entrada, donde "name" es la clave y "Eva" es el valor.

El nombre es un string y debe estar rodeado de comillas dobles ""

TypeScript

TypeScript no incluye un tipo de datos nativo "diccionario" porque los objetos en TypeScript se pueden utilizar como si fueran diccionarios.

Por ejemplo aquí tienes un objeto JSON que representa a una persona:

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

A continuación tienes el mismo objeto representado como un objeto de TypeScript:

{
    name: "Eva",
    age: 30
}

¿Alguna diferencia? 🤗

¡Fíjate que la única diferencia es que JSON utiliza comillas dobles para las claves!

Y siempre debes utilizar comillas dobles al representar un string, nunca puedes utilizar comillas simples: 'Eva' no es válido en JSON.

Instala Bun

Crea el directorio json:

> mkdir json
> cd json

Importa la biblioteca bun:

> bun add bun
> bun add -d @types/bun

Verás que se ha creado un fichero JSON package.json que define las dependencias del proyecto:

{
  "dependencies": {
    "bun": "^1.2.2"
  },
  "devDependencies": {
    "@types/bun": "^1.2.2"
  }
}

JSON nos permite convertir una estructura a un string con la función JSON.stringify().

Crea un fichero write.ts.

let eva = {
    name : "Eva",
    age: 30
}

let json = JSON.stringify(eva)

console.log(json)

Puedes ver que port pantalla se imprime un string: name i age van entre comillas dobles (""):

> bun run .\write.ts
{"name":"Eva","age":30}

Una vez que tenemos el JSON convertido en string, podemos escribirlo en varios destinos, como una respuesta de API, un archivo o cualquier otra cosa.

Por ejemplo, escribe el string en el fichero eva.json:

export {}

let eva = {
    name : "Eva",
    age: 30
}

let json = JSON.stringify(eva)

await Bun.write("eva.json", json)

Tienes que añadir export {} al principio del fichero porque las expresiones await solo se permiten en el nivel superior de un archivo cuando ese archivo es un módulo.

Todas las operaciones E/S has de ser asíncronas!

Si ejecutas el script puedes ver que se crea un fichero eva.json que tanto tu como cualquier lenguaje de programación puede leer sin problemas:

> bun run .\write.ts
> Get-Content .\eva.json             
{"name":"Eva","age":30}

Python

Aquí tienes el objeto JSON que representa a una persona:

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

A continuación tienes el mismo objeto representado como un diccionario en Python:

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

¿Alguna diferencia? 🤗

Instala Python con Scoop.

Crea el script read.py:

import json

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

eva = json.loads(str)

print(eva)  

Con la función loads puedes leer un str y convertirlo en un diccionario Python.

> python.exe .\read.py
{'name': 'Eva', 'age': 30}

Modifica el fichero read.py para leer el fichero eva.json que a creado un script de TypeScript:

import json

with open('eva.json', 'r') as file:
    eva = json.load(file)

print(eva["name"])  

Puedes ver que TypeScript y Python pueden compartir datos 💞:

python.exe .\read.py
Eva

El problema es que pueden compartir datos con cualquier lenguaje que pase por ahí, incluso contigo, no son un buen ejemplo de fidelidad, JSON es muy, muy simple 😒

Actividades

1.- Aquí tienes un objeto Typescript que representa un libro:

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

Modifica:

  • El script write.ts para que escriba el libro en book.json
  • El script read.py para que lea el libro e imprima el título.

write.ts

export { }

let book = {
    book: "The Art of Computer Programming",
    author: "Donald Knuth"
}

let json = JSON.stringify(book)

await Bun.write("book.json", json)

read.py

import json

with open('book.json', 'r') as file:
    book = json.load(file)

print(book["author"])  

Funciona ! 😀

> bun run .\write.ts
PS C:\Users\david\Workspace\ts-json> python.exe .\read.py
Donald Knuth

2.- Importa el fichero package.json y imprime por consola las dependencias del proyecto:

export{}

let str = await Bun.file("package.json").text()
let pack = JSON.parse(str)
console.log(pack.dependencies)

Fíjate que lo que se imprime es un objeto TypeScript, el nombre de la propiedad no va entre "":

{
  bun: "^1.2.2",
}

Lista

Una lista es un conjunto ordenado de elementos que puede accederse por posición.

A continuación tienes un ejemplo de una lista:

[ 1, "hello", false ]

Python

Aquí tienes una lista en JSON:

[1 ,"hello", false]

Y la misma lista en Python 🤗 :

[1 ,"hello", False]

Crea el script write.py:

import json

list = [1 ,"hello", False]

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

Con la función json.dump conviertes un objeto Python en un str JSON.

> python.exe .\write.py

TypeScript

Una lista "json" se representa con un array TypeScript: 🤗

[1 ,"hello", False]

Crea el fichero read.ts para leer los datos del fichero data.json:

export{}

let str = await Bun.file("data.json").text()
let list = JSON.parse(str)

console.log(list)
> python.exe .\write.py
> bun .\read.ts
[ 1, "hello", false ]

Actividad

Modifica el fichero write.py:

import json

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

with open('data.json', 'w') as file:
    json.dump(customers, file)

Ejecuta el script para crear el fichero data.json:

> python.exe .\write.py

Modifica el fichero read.ts para que importe los datos de data.json e imprima sólo los nombres de los clientes:

Tipos

Tipos básicos

JSON sólo tiene tres tipos básicos: string, number y boolean!

string

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

Un string tiene estas propiedades:

  • Un string consiste en cero o más caracteres Unicode entre comillas dobles ("").
  • Un string entre comillas simples (') no es válido – a diferencia de TypeScript y Python.

Además, un string puede contener los siguientes caracteres "backslash-escaped":

\" Double quote
\\ Backslash
\/ Forward slash
\b Backspace
\f Form feed
\n Newline
\r Carriage return
\t Tab
\u Trailed by four hex digits

number

{
  "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
}

Los números siguen el formato de coma flotante de doble precisión de TypeScript y tienen las siguientes propiedades:

  • Los números siempre están en base 10 (sólo se permiten los dígitos del 0 al 9) sin ceros iniciales.
  • Los números pueden tener una parte fraccionaria que comienza con un punto decimal (.).
  • Los números pueden tener un exponente de 10, que se representa con la notación eo E con un signo positivo o negativo para indicar una exponenciación positiva o negativa.
  • No se admiten los formatos octal y hexadecimal.
  • A diferencia de TypeScript, los números no pueden tener un valor de NaN (no es un número para números no válidos) o Infinity.

boolean

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

Los booleanos tienen las siguientes propiedades:

  • Los booleanos sólo pueden tener un valor de true o false.
  • El valor true o false en el lado derecho de ambos puntos (:) no está rodeado de comillas.

null

Aunque técnicamente no sea un tipo de valor, null es un valor especial en JSON.

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

Los valores nulos tienen las siguientes características:

  • No están rodeados de comillas
  • Indica que una clave/propiedad no tiene valor
  • Actúa como marcador de posición

"Tipos" derivados

JSON sólo tiene tres tipos básicos porque con un string puedes representar cualquier tipo de datos.

Sólo hace falta que todos nos pongamos de acuerdo en cómo representamos un dato.

En este enlace tienes la guía de estilo de Google respecto a JSON: JSON Style Guide.

Fecha

Por ejemplo, Google prefiere que las fechas sigan el formato RFC 3339:

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

La fecha anterior proporciona un desplazamiento de la hora universal coordinada (UTC) (desde UTC/GMT—Hora media de Greenwich) de -5 horas, que es la hora estándar del este de EE.UU. Tenga en cuenta que la RFC 3339 es un perfil de la ISO 8601.

const date = new Date()

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

La principal diferencia es que la ISO 8601 de la Organización Internacional de Estándares permite sustituir a la T (que separa la fecha y la hora) por un espacio, y la RFC 3339 no lo permite.

Valores de latitud/longitud

Las API geográficas (p. ej., Google Maps) y las API relacionadas con un sistema de información geográfica (SIG) utilizan datos de latitud/longitud. Para garantizar la coherencia, Google recomienda que los datos de latitud/longitud sigan el estándar ISO 6709.

Según Google Maps, las coordenadas del Empire State Building en la ciudad de Nueva York son 40,748747° N, 73,985547° O y estarían representadas en JSON..

{
    "empireStateBuilding": "40.748747-73.985547"
}

Este ejemplo sigue el formato ±DD.DDDD±DDD.DDDD, con las siguientes convenciones:

  • La latitud es lo primero.
  • La latitud norte (del ecuador) es positiva.
  • La longitud este (del primer meridiano) es positiva.
  • La latitud/longitud se representa con un string. No puede ser un número debido al signo menos.

Estructuras complejas

Combinando diccionarios y listas, y con sólo tres tipos básicos, puedes representar cualquier tipo de datos:

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

A continuación tienes el diagrama de clases correspondiente:

classDiagram
    direction LR

    class Person {
        id: number
        name: string
    }

    class Address {
        street: String
        city: String
    }

    Person --> Address

Referencias repetidas

Cuando transformamos una estructura de datos en un texto JSON, el proceso no codifica referencias, sinó que va codificando los objetos que encuentra a su paso.

Por ejemplo, en un proyecto el owner y el mantainer son del tipo User:

classDiagram
    direction LR

    class Project {
        name: string
    }

    class User {
        name: String
    }

    Project --> User : owner
    Project --> User : mantainer

A continuación tienes un ejemplo en que en el proyecto "Quantum System", owner y mantainer hacen referencia al mismo objecto de tipo User:

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))

Si ejecutas el código, puedes ver que el owner y el matainer se codifican som si fueran dos objetos distintos.

> bun run .\test.ts
{"name":"Quantum System","owner":{"name":"David"},"mantainer":{"name":"David"}}

Esto significa que puede haber datos repetidos porque un objeto puede hacer referencia al mismo de forma directa o indirecta.

flowchart LR
    project["Quantum System"]
    david["🐱 David"]
    project -- owner --> david
    project -- mantainer --> david

En nuestro ejemplo, si guardamos el proyecto en un fichero y lo volvemos a leer, tendremos un proyecto en que el owner y el mantainer son dos objetos distintos aunque tengan el mismo nombre:

export {}

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 }

await Bun.write("project.json", JSON.stringify(project))

project = JSON.parse(await Bun.file("project.json").text())
project.owner.name = "Eva"

console.log(project.mantainer.name) // Eva

Si ejecutas el script, puedes ver que si cambio el nombre del owner, el nombre del mantainer no cambia porque son dos objetos distintos:

> bun run .\test.ts
David

Debes evitar un código de este tipo si vas a compartir estos datos con JSON 💩 !

No hay referencias, hay redundancia!

Referencias circulares

Como ya se ha explicado antes, la serialización no tiene en cuenta si un objeto ya se había serializado.

Esto significa que además de la redundancia, la serialización sólo funciona con árboles: un objeto raíz, ramificaciones y todo termina en hojas.

flowchart TB
    boeing["✈ Boeing 747"]
    pilot["🤠 Peter"]
    boeing -- pilot --> pilot
    passengers["[🤗 Julia, 🫣 Mary, 🙄 Tom, 😮‍💨 Jane, 😮 Albert, 🤢 Josephine, 🥴 Clara, 😵‍💫 Mike"]
    boeing -- passengers --> passengers

Se comienza con un objeto y se van procesando de forma recursiva todas las propiedades que son referencias hasta que no queda ningún objeto referenciado por procesar.

Por ejemplo, si David tiene una referencia directa o indirecta con Esther, y Esther tiene una referencia directa o indirecta con David ...

flowchart LR
    david["😺 David"]
    esther["🦝 Esther"]
    david --> esther
    esther --> david

la recursión no terminará nunca 💫💫💫💫💫 ...

export {}

type Person =  { name: string, couple: Person | null }

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

await Bun.write("david.json", JSON.stringify(david))

pero JSON no lo permite 😱 ... no cree en el amor enterno 💞 ...

> bun run .\test.ts
4 | 
5 | const david: Person = { name: "David", couple:null }
6 | const esther: Person = { name: "Esther", couple: david}
7 | david.couple = esther
8 | 
9 | await Bun.write("david.json", JSON.stringify(david))
                                       ^
TypeError: JSON.stringify cannot serialize cyclic structures.
      at C:\Users\david\Workspace\json\test.ts:9:36

que puede hacer el pobre gatito 🙀🙀 ??

Cambiar el disseño 😼 ... y no poner fecha de finalización (siempre optimista, pensar que las parejas son para siempre 💞) ...

classDiagram
    direction LR

    class Person {
        name: string
    }

    class Couple {
        start: string
    }

    Couple --> Person : one
    Couple --> Person : two

Y ya podemos escribir el código:

export {}

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}

await Bun.write("david-and-esther.json", JSON.stringify(couple))

Y JSON dice que sí 😀, además es más romántico 🌹:

> bun run .\test.ts
> Get-Content .\david-and-esther.json
{"start":"Un noche de lluví­a con luna en el cielo","one":{"name":"David"},"two":{"name":"Esther"}}

El que la fecha tenga que estar en formato RFC 3339 es un estilo Google ... el start de este còdigo también es una fecha "tipo" string 🌈.

Pero hay un problema, una pareja es cosa de dos, one y two que no es lo mismo que first y second aunque algunos lo interpretarán de esta manera, y el he y she hace tiempo que no se admite como exclusivo ...

La próxima vez dieseñamos un zoo 😒: 🐯 🦒 🐮 🐰 🦓 🐼 🐨 🐑 🦛 🦏 🦬 🐆

Actividad - Vuelos Pájaro Bobo

La empresa "Vuelos Pájaro Bobo" es una start-up que necesita un sistema básico de gestión de vuelos.

A continuación tienes el diseño de la estructura de datos:

classDiagram
    direction TB

    class Flight {
        << 🐦‍🔥 >>
        id: string 🔑
        ufoSeen: boolean 🛸
    }
    
    class Plane {
        << ✈ >>
        name: string 🔑
        seats: number 💺
    }

    class Airport {
        << ⛩ >>
        code: srting 🔑
        name: string
        city: string
        country: string 
    }
    
    class Passenger {
        << 🙂 >>
        name: string  🔑
        face?: string
        }
    
    Flight --> Plane
    Flight --> "1..* 🤗 🫣 🙄 😮" Passenger 
    
    Flight --> Airport : departure 🛫
    Flight --> Airport: arrival 🛬

De momento sólo tiene vuelos desde "Barcelona", código BCN, y sí, los pasajeros se identifican por el nombre.

Por tanto, si en un vuelo ya hay un pasajero con nombre "Eva" y otra "Eva" quiere viajar, o se cambia el nombre o no puede viajar en ese vuelo 👏.

A continuación tienes un ejemplo para el vuelo bcn-cdg-tomorrow-morning-after-breakfast.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": "😵‍💫" }
    ]
}

Tienes que completar el script bobo.ts para que permita añadir pasajeros:

export {}

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

// 👉 tu turno! 

Para gestionar ficheros mira esta documentación: https://bun.sh/docs/api/file-io

Al finalizar tienes que crear un binario ejecutable:

> bun build ./bobo.ts --compile --outfile bobo
  [31ms]  bundle  1 modules
 [394ms] compile  bobo.exe

Este binario se puede ejecutar direcamente en cualquier sistema Windows:

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