JSON Toolkit

JSON vers SQL : instructions INSERT, conception de tables et pièges de mapping de types

L’essentiel : Transformer du JSON en INSERT SQL cache le découpage en tables imbriquées, NULL contre champ absent, l'échappement et les écarts de dialectes. Instructions exécutables et tableaux comparatifs pour chaque piège.

Intermédiaire~10 min de lecturePublié le 2026-10-31Mis à jour le 2026-10-31
Illustration du guide JSON vers INSERT SQL et conception de tables

L'essentiel : décidez quelle entité métier représente une ligne avant de concevoir les tables, puis écrivez des VALUES multi-lignes explicites dans une seule transaction ; réglez le nommage des champs et le mapping de types avant l'import plutôt que de les corriger en SQL.

JSON décrit une forme ; SQL décrit aussi des contraintes. Quand vous faites passer la première dans le second, chaque contrainte que vous n'avez pas écrite revient plus tard sous forme de données sales.

1. Décidez d'abord : une table ou un enregistrement par ligne JSON

Regardez la couche la plus externe des données avant de concevoir quelque table que ce soit. Une mauvaise décision ici oblige à réécrire chaque INSERT ensuite.

Structure JSON Conception de table suggérée Risque principal
Tableau plat d'objets [{...},{...}] Une table, plusieurs lignes Des clés incohérentes entre enregistrements laissent la plupart des colonnes d'union à NULL
Objet enveloppant un tableau {"data":[...],"total":100} Une table, prendre seulement data Écrire total dans chaque ligne déplace le sens du champ
Objet imbriqué {"user":{"name":"x"}} Table parente plus colonnes aplaties, ou parente plus table liée Expansion incontrôlée, noms de colonnes dupliqués
Champ tableau {"tags":["a","b"]} Table enfant, ou colonne JSON/TEXT Une relation un-à-plusieurs écrasée dans une cellule et plus interrogeable par tag
Clés incohérentes Colonnes d'union, ou tables séparées par type « Clé manquante » et « valeur null » se fondent dans le même NULL

Il n'y a qu'un critère : quelle entité métier représente une ligne ? Si une ligne est une commande, les lignes d'articles doivent vivre dans leur propre table. Si une ligne est un utilisateur, les tags appartiennent à une table enfant ou à une colonne JSON, pas à une chaîne jointe par des virgules.

2. D'un tableau à INSERT : le plus petit modèle exécutable

La forme la plus sûre liste toujours explicitement les noms de colonnes et ne s'appuie jamais sur l'ordre des colonnes :

CREATE TABLE users (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  email TEXT UNIQUE
);

INSERT INTO users (id, name, email) VALUES
  (1, 'Ada', 'ada@example.com'),
  (2, 'Linus', NULL);

Trois détails comptent :

  • Écrivez chaque nom de colonne. Quand le schéma gagne plus tard une colonne, rien ne se décale vers le mauvais champ.
  • Le VALUES multi-lignes fonctionne dans SQLite, PostgreSQL et MySQL, et économise un grand nombre d'allers-retours par rapport à une boucle d'insertions mono-ligne.
  • NULL est un mot-clé ; ne le mettez pas entre guillemets. Écrire 'NULL' stocke une chaîne de quatre caractères.

Si les clés JSON ne correspondent pas aux noms de colonnes (userId contre user_id), ajoutez une couche de mapping. Ne devinez pas avec COALESCE dans le SQL. Corriger les noms de champs dans la couche de données coûte bien moins cher que de les rafistoler dans les requêtes.

3. Objets imbriqués : une table parente plus une table enfant

Les commandes sont le cas un-à-plusieurs classique :

INSERT INTO orders (id, customer_name, total_cents) VALUES
  (1001, 'Ada', 12900);

INSERT INTO order_items (order_id, sku, quantity) VALUES
  (1001, 'A-1', 2),
  (1001, 'B-7', 1);

Cela correspond au JSON {"id":1001,"customer":"Ada","total":129.00,"items":[{"sku":"A-1","quantity":2},{"sku":"B-7","quantity":1}]}.

Points clés :

  • La longueur du tableau items détermine le nombre de lignes enfants ; la table parente reçoit exactement une ligne.
  • Stockez l'argent en centimes entiers (total_cents), jamais en FLOAT.
  • Donnez à la table enfant une clé étrangère order_id et committez les deux inserts dans une seule transaction, sinon le parent peut exister alors que ses articles manquent.
  • Si les données sont en lecture seule et que personne ne filtre par ligne d'article, une colonne JSON est acceptable (PostgreSQL jsonb, MySQL JSON, ou SQLite TEXT avec les fonctions json1), mais acceptez le coût : pas d'indexation et requêtes malcommodes.

Aplatissez les objets imbriqués fixes comme user.address en colonnes (address_city, address_zip). Ne découpez en table que lorsque l'imbrication peut continuer à croître ; découper une forme fixe ne fait qu'ajouter des JOIN.

4. NULL contre champ absent : SQL n'a pas de « clé absente »

JSON permet qu'une clé soit entièrement absente. Les colonnes SQL sont fixes. Après un import, les deux cas sont identiques — NULL — même s'ils peuvent signifier des choses différentes :

Situation JSON Ce que montre la base Traitement suggéré
La clé est absente NULL Ajouter un défaut à la définition de table, ou une colonne marqueur _present
La clé existe avec null NULL Signifie que l'utilisateur a explicitement effacé la valeur
La clé existe avec une chaîne vide '' Distinguable de NULL, mais facile à confondre dans les requêtes
Champ numérique absent NULL Ne laissez jamais 0 prétendre être « manquant »

Deux erreurs courantes :

  1. Utiliser une chaîne vide au lieu de NULL. Plus tard, WHERE email IS NULL rate ces lignes, et votre métrique « utilisateurs sans e-mail » sous-compte silencieusement.
  2. Tester avec col = NULL. Dans la logique à trois valeurs de SQL, NULL = NULL s'évalue à NULL, pas à TRUE. Il faut écrire IS NULL :
SELECT COUNT(*) FROM users WHERE email IS NULL;

Si vous devez préserver le fait qu'une clé était présente ou non, ajoutez une colonne marqueur booléenne plutôt que de l'inférer depuis NULL. Le contrat d'API devrait aussi le préciser ; voir Conception d'API JSON et nommage des champs.

5. Quoting et échappement : ne concaténez jamais des chaînes SQL

Les chaînes JSON peuvent contenir des guillemets simples, des guillemets doubles, des barres obliques inverses, des retours à la ligne et même \u0000. Avec des chaînes construites à la main, un échappement oublié décale le reste de l'instruction, et au pire devient une injection SQL.

En SQL standard, un guillemet simple s'échappe en le doublant :

INSERT INTO notes (id, content) VALUES (1, 'It''s a ''quoted'' value');

Le code de production devrait utiliser la liaison de paramètres et laisser le pilote gérer l'échappement et les types :

// PostgreSQL uses $1/$2, MySQL uses ?; this example uses node-postgres
await client.query('INSERT INTO users (id, name) VALUES ($1, $2)', [row.id, row.name]);

N'échappez à la main que lorsque vous devez exporter un fichier .sql, et alors au minimum :

  • remplacez ' par '' ;
  • si les retours à la ligne sont préservés, confirmez que la base cible accepte les littéraux de chaîne multi-lignes ;
  • supprimez \u0000, car la plupart des bases rejettent le NUL dans les chaînes ;
  • MySQL traite par défaut la barre oblique inverse comme un caractère d'échappement, donc \ doit devenir \\, ou passez en mode standard.

Les guillemets doubles de JSON n'ont pas besoin d'échappement à l'intérieur d'une chaîne SQL entre guillemets simples. Ajouter une couche d'échappement supplémentaire est exactement la façon dont des barres obliques inverses parasites finissent dans vos données.

6. Mapping de types : où atterrissent les six valeurs JSON en SQL

JSON n'a que chaîne, nombre, booléen, null, objet et tableau. SQL a un système de types bien plus fin. Les erreurs surviennent surtout sur la précision et la sémantique plutôt que sur la syntaxe :

Valeur JSON Type SQL suggéré Où ça casse
Entier INTEGER / BIGINT Au-delà de 2^53, le côté JavaScript a déjà perdu la précision, donc la valeur stockée est aussi fausse
Décimal, monnaie NUMERIC / DECIMAL FLOAT est du flottant binaire ; 0.1 + 0.2 n'est pas exact
Chaîne TEXT / VARCHAR(n) Les limites de longueur tronquent ; la collation change les comparaisons
Booléen BOOLEAN / TINYINT(1) SQLite n'a pas de type booléen et stocke 0/1
null NULL Indistinguable d'un champ manquant
Chaîne de date DATE / TIMESTAMP / TIMESTAMPTZ Fuseau horaire perdu, millisecondes tronquées, formats invalides
Chaîne d'énumération TEXT avec CHECK, ou une table d'énumération Les différences de casse et les valeurs sales n'ont aucune contrainte
Objet / tableau Une table enfant, ou JSON / JSONB Stocké comme chaîne, il ne peut être ni indexé ni interrogé

Les booléens cachent le bug le plus subtil : false en JSON devient 0 dans MySQL TINYINT(1), les allers-retours semblent corrects, et personne ne remarque. Mais dès qu'un code fait if (value) sur la chaîne "false", elle est évaluée comme vraie. Les scripts de conversion devraient appeler explicitement Boolean(value).

PostgreSQL JSONB parse et déduplique les clés, en gardant la dernière occurrence, ce qui convient à l'interrogation ; JSON préserve le texte original et l'ordre des clés. SQLite JSON n'est que du TEXT plus un ensemble de fonctions, sans véritable contrainte de type. Avant de choisir, demandez si cette table doit filtrer sur des champs à l'intérieur du JSON.

7. Insertions en masse et transactions : lotir, committer, rendre réexécutable

À mesure que le volume croît, trois choses décident si un import reste maîtrisable :

  1. Lotir. Une boucle de N INSERT mono-ligne coûte N allers-retours réseau. Combinez 500 à 1000 lignes en un seul VALUES multi-lignes, ou utilisez le chargeur en masse de la base (PostgreSQL COPY, MySQL LOAD DATA, insertions multi-lignes dans une transaction SQLite).
  2. Utilisez une transaction. Tout ou rien, pour qu'une table à moitié chargée ne survive jamais à une erreur :
BEGIN;
INSERT INTO users (id, name) VALUES (1, 'Ada'), (2, 'Linus');
INSERT INTO order_items (order_id, sku, quantity) VALUES (1, 'A-1', 2);
COMMIT;
  1. Rendez-le idempotent. Les scripts d'import sont réexécutés constamment. Utilisez un upsert au lieu d'un supprimer-puis-insérer :
Base Forme idempotente
PostgreSQL / SQLite INSERT ... ON CONFLICT (id) DO UPDATE SET ...
MySQL INSERT ... ON DUPLICATE KEY UPDATE ...

ON CONFLICT exige une contrainte d'unicité ou une clé primaire, sinon il ne se déclenche jamais. Quand il n'y a pas de clé naturelle, construisez un index unique sur une clé métier comme email. Quand il n'y a aucune clé, ajoutez un identifiant stable côté JSON — ne comptez jamais sur un identifiant auto-incrémenté pour l'idempotence, car une réexécution insérera des doublons.

8. Différences de dialectes en un coup d'œil

Capacité SQLite PostgreSQL MySQL
VALUES multi-lignes Oui Oui Oui
Upsert ON CONFLICT ON CONFLICT ON DUPLICATE KEY UPDATE
Clé auto-incrémentée INTEGER PRIMARY KEY GENERATED ... AS IDENTITY AUTO_INCREMENT
Type booléen Aucun, stocke 0/1 boolean TINYINT(1)
Import en masse Insertions multi-lignes dans une transaction COPY LOAD DATA
Support JSON TEXT plus fonctions json1 json / jsonb JSON
Quoting d'identifiants Guillemets doubles Guillemets doubles Accents graves (par défaut)

Pour garder un seul fichier SQL fonctionnel sur les trois, évitez au moins ON CONFLICT, les accents graves et SERIAL. Pour les projets multi-bases, générez séparément le schéma et les instructions d'upsert ; n'attendez pas d'un seul script qu'il convienne à tous.

9. Trois vérifications par sondage après l'import

Avant de supprimer le fichier source, vérifiez trois choses :

  1. Nombre de lignes. SELECT COUNT(*) FROM users; doit égaler la longueur du tableau JSON. Une ligne manquante signifie généralement que le dernier enregistrement a été tronqué.
  2. La colonne au taux de NULL le plus élevé. Cela indique des noms de clés incohérents ou une imbrication non développée. Revenez vérifier si la source utilise userId et user_id.
  3. Monnaie et longs identifiants. Prenez les valeurs maximale et minimale et confirmez que ni la conversion en flottant ni celle en entier ne les a tronquées.

Si vous devez d'abord comprendre la forme tabulaire, utilisez l'approche d'aplatissement de Conversion JSON, CSV et données tabulaires. Si le public est constitué de collègues qui veulent simplement lire les données, les quatre façons de convertir JSON en Excel demandent moins de travail que de monter une base de données.

Pourquoi les nombres perdent-ils de la précision après l'import de JSON en SQL ?

Les entiers au-delà de 2^53 ont déjà perdu de la précision pendant le JSON.parse de JavaScript, et les écrire dans BIGINT ne peut pas la restaurer. Ces identifiants devraient être des chaînes en JSON et être convertis en BIGINT ou TEXT pendant l'import.

Que faut-il utiliser comme clé primaire quand le JSON n'a pas d'id ?

Laissez la base en générer une, ou créez un UUID stable avant l'import. N'utilisez jamais l'index de tableau comme id : une réexécution réordonne les données et pointe les mauvaises lignes vers les mauvais enregistrements. Un index unique sur une clé métier (e-mail, numéro de commande) permet aussi les upserts.

Que faire quand un INSERT en masse heurte un conflit de clé unique ?

Remplacez le simple INSERT par l'upsert correspondant à votre dialecte, et confirmez que la colonne en conflit a une contrainte d'unicité. Supprimer d'abord puis insérer fonctionne aussi, mais cela doit se faire dans une seule transaction et vous acceptez une brève fenêtre sans lignes.

Les tableaux imbriqués exigent-ils toujours une table enfant ?

Non. Pour un affichage en lecture seule sans interrogation par sous-élément, une colonne JSON est plus simple. Dès qu'il faut filtrer ou agréger par tag ou SKU, une table enfant avec index est le choix durable. Le facteur décisif est la façon dont vous interrogez, pas la forme des données.