Este artículo es la segunda parte de Unmarshal JSON en Bash (Parte I).
Al final de la primera parte vimos cómo el MVP (minimum viable product) no producía el resultado esperado para keys en el documento JSON cuyo valor es un array o un object.
En esta segunda parte, vamos a resolver este problema.
Documento de tipo object
En cierto punto del artículo anterior indicaba que la función unmarshal
sólo soporta documentos JSON de tipo object
, es decir { ... }
.
En primer lugar, exploramos qué otros tipos de documentos podemos encontrarnos en JSON.
Otros tipos de documentos JSON
array
Si el documento JSON es de la forma:
[ "jim", "pam", "michael"]
Las keys son valores numéricos, es decir, el índice del elemento en el array:
$ jq -r 'keys[]' invalid.json
0
1
2
En Bash no podemos definir una variable usando un número, lo que resutaría en 1="Dunder Mifflin"
, por ejemplo, dando lugar al error not a valid identifier
.
Otras combinaciones de elementos, si el JSON es un array
, dan el mismo resultado:
[
{
"manager": "michael"
},
{
"employees": ["pam","jim"]
}
]
Por tanto, descartamos dar soporte a los documentos JSON de tipo array
.
string
, number
y whitespace
Si el documento únicamente contiene sólo un elemento de tipo string
(por ejemplo "pam"
), es un documento JSON válido, pero no tenemos ninguna key:
$ jq -r 'keys[]' invalid.json
jq: error (at invalid.json:0): string ("pam") has no keys
Lo mismo sucede si el contenido del documento JSON es un número, como 1
o -0.7
:
$ jq -r 'keys[]' invalid.json
jq: error (at invalid.json:0): number (1) has no keys
Cómo determinar el tipo de documento JSON
Jq proporciona la función type
que nos sirve, precisamente, para identificar de qué tipo es el argumento que se le pasa.
Pasamos el documento JSON completo a la función type
para determinar que se trata de un object
; si no es así, salimos:
document_type=$(jq -r '. | type' "$doc")
if [[ $document_type != "object" ]]; then
echo "'$doc' is of an unsupported type: '$document_type' (only 'object' is supported)"
exit 0
fi
De esta forma:
$ bash main.sh --doc invalid.json
'invalid.json' is of an unsupported type: 'array' (only 'object' is supported)
keys con valores de tipo array
u object
Ahora hemos restringido los documentos que vamos a procesar, pero todavía podemos encontrar keys de tipo array
y object
:
{
"customer": "Dunder Mifflin Scranton",
"uuid": "3f6b0814-e923-415b-9fd8-db9407e69546",
"active": true,
"employees": ["pam", "jim", "michael"]
}
El resultado, al intentar usarlo en el script:
# ...
# Use variables
echo "customer: $customer"
echo "uuid: $uuid"
echo "active: $active"
for e in ${employees[@]}; do
echo " - $e"
done
Da como resultado:
$ bash mvp_unmarshal.sh --doc customer.json
customer: Dunder Mifflin Scranton
uuid: 3f6b0814-e923-415b-9fd8-db9407e69546
active: true
- [
- "pam",
- "jim",
- "michael"
- ]
Discriminando las keys
Podemos usar la función de Jq type
para determinar qué tipo de valor contiene cada key:
for k in "${keys[@]}"; do
declare -n ref="$k"
# Determine the type of the value for every key
type="$(jq -r --arg k "$k" '.[$k] | type' "$document")"
case $type in
"string" | "boolean" | "number")
ref=$(jq -r --arg key "$k" '.[$key]' "$document")
;;
*)
continue
;;
esac
ref=$(jq -r --arg key "$k" '.[$key]' "$doc")
done
El resultado no es exactamente lo que buscábamos, pero vamos en la buena dirección; las keys soportadas se procesan con normalidad y las “problemáticas”, se ignoran (por ahora):
$ bash mvp_unmarshal.sh --doc customer.json
customer: Dunder Mifflin Scranton
uuid: 3f6b0814-e923-415b-9fd8-db9407e69546
active: true
Convertir JSON array en Bash array
Ya hemos usado anteriormente un mecanismo para convertir de array en JSON a array en Bash: mediante mapfile
, para las keys
del documento.
Vamos a usar el mismo método para aquellas keys que son de tipo array:
Añadimos un nuevo case:
"array")
mapfile -t "$k" < <(jq -r --arg key "$k" '.[$key][]' "$doc")
;;
Esto ya nos proporciona el resultado que buscamos:
$ bash mvp_unmarshal.sh --doc customer.json
customer: Dunder Mifflin Scranton
uuid: 3f6b0814-e923-415b-9fd8-db9407e69546
active: true
- pam
- jim
- michael
¿Qué pasa con los object
?
Si tratamos los object
como array
, como usamos el iterator para obtener los elementos del array, en el objeto obtenemos los valores de cada una de las keys en el object (al menos, del primer nivel):
{
"customer": "Dunder Mifflin Scranton",
"uuid": "3f6b0814-e923-415b-9fd8-db9407e69546",
"active": true,
"employees": [
"pam",
"jim",
"michael"
],
"organization": {
"manager": "michael",
"branch": "scranton",
"address": {
"pobox": "08080",
"street": "Fictional Street, Scranton"
}
}
}
Lo que da como resultado:
bash mvp_unmarshal.sh --doc customer.json
customer: Dunder Mifflin Scranton
uuid: 3f6b0814-e923-415b-9fd8-db9407e69546
active: true
- pam
- jim
- michael
organization:
- michael
- scranton
- {
- "pobox": "08080",
- "street": "Fictional Street, Scranton"
- }
Para evitar introducir mayor complejidad, lo ideal sería que cualquier cosa que haya almacenada en la key se considere el valor de la key, sin evaluar su contenido.
Es decir, lo consideraremos un string en Bash, ya que no existe el concepto de object
.
Para conseguirlo, eliminamos el operador iterator
de Jq sobre la key:
"object")
mapfile -t "$k" < <(jq -r --arg key "$k" '.[$key]' "$doc")
;;
El resultado es que todo el contenido de la key organization
se almacena en un array de Bash:
organization:
- {
- "manager": "michael",
- "branch": "scranton",
- "address": {
- "pobox": "08080",
- "street": "Fictional Street, Scranton"
- }
- }
No es lo que queremos, pero podemos compactar toda la estructura del contenido de la key mediante la opción -c
(--compact-output
) de Jq:
"object")
mapfile -t "$k" < <(jq -r -c --arg key "$k" '.[$key]' "$doc")
;;
Esto ya proporciona la salida que buscamos:
organization:
- {"manager":"michael","branch":"scranton","address":{"pobox":"08080","street":"Fictional Street, Scranton"}}
Si añadimos esta misma opción para los arrays, podemos gestionar arrays de objects:
{
"customer": "Dunder Mifflin Scranton",
"uuid": "3f6b0814-e923-415b-9fd8-db9407e69546",
"active": true,
"employees": [
"pam",
"jim",
"michael"
],
"organization": [
{
"manager": "michael",
"branch": "scranton",
"address": {
"pobox": "08080",
"street": "Fictional Street, Scranton"
}
},
{
"manager": "david",
"branch": "slough",
"address": {
"pobox": "90909",
"street": "Werhnam Street, Slough"
}
}
]
}
El resultado es:
$ bash mvp_unmarshal.sh --doc customer.json
customer: Dunder Mifflin Scranton
uuid: 3f6b0814-e923-415b-9fd8-db9407e69546
active: true
- pam
- jim
- michael
organization:
- {"manager":"michael","branch":"scranton","address":{"pobox":"08080","street":"Fictional Street, Scranton"}}
- {"manager":"david","branch":"slough","address":{"pobox":"90909","street":"Werhnam Street, Slough"}}
Esta solución permite, si es necesario, usar Jq para procesar el contenido de estos objetos anidados (mediante echo $k | jq '.'
)
Convertirlo en una función
Lo último que voy a hacer es empaquetar el código en forma de función.
unmarshal() {
local document="$1"
# If it's not a JSON object, exit
document_type=$(jq -r '. | type' "$document")
if [[ $document_type != "object" ]]; then
echo "'$document' is of an unsupported type: '$document_type' (only 'object' is supported)"
exit 1
fi
# Save document keys in a Bash array
declare -a keys
mapfile -t keys < <(jq -r 'keys[]' "$document")
for k in "${keys[@]}"; do
# Declare reference variable
declare -n ref="$k"
type="$(jq -r --arg k "$k" '.[$k] | type' "$document")"
case $type in
"string" | "boolean" | "number")
# echo "type: $type"
ref=$(jq -r --arg key "$k" '.[$key]' "$document")
;;
"array")
mapfile -t "${!ref}" < <(jq -r -c --arg key "$k" '.[$key][]' "$document")
;;
"object")
mapfile -t "$k" < <(jq -r -c --arg key "$k" '.[$key]' "$document")
;;
*)
echo "'$k' is of an unsupported type: '$type' (only 'string', 'bool', 'number' and 'null' are supported)"
exit 1
;;
esac
done
}