Migraciones Drupal

Drupal Migrate API: Importa Contenido desde JSON en Drupal 11

Guía práctica de la Migrate API en Drupal 11: migra contenido desde JSON con migrate_plus, define source, process y destination en YAML, depura con drush y crea tus propios plugins de proceso.

por Santi López ·

Qué es la Migrate API

En resumen: Guía completa de la Migrate API en Drupal 11 para importar contenido desde JSON, CSV o APIs externas. Define source, process y destination en YAML, depura con drush y crea plugins de proceso personalizados.

  • Importa contenido desde JSON con migrate_plus y el plugin url
  • Define normalizadores para media, taxonomías y relaciones entre entidades
  • Depura con drush migrate:import, —migrate-debug y migraciones incrementales
  • Aplica rollback, reset-status y plugins de proceso personalizados

Cuando necesitas importar contenido a Drupal desde un sistema externo — una API REST, un fichero JSON, una base de datos heredada o un CSV — la Migrate API es la herramienta nativa para hacerlo. No es un módulo puntual: es un framework completo que gestiona el ciclo de vida de la importación, desde la extracción del dato hasta la creación de entidades en Drupal, pasando por transformaciones, validaciones y control de errores.

Es importante distinguir dos escenarios:

  • Migraciones de contenido custom (lo que cubre esta guía): importas datos desde una fuente externa (JSON, XML, CSV) usando el plugin url de migrate_plus y ficheros YAML que defines tú.
  • Ruta de upgrade D7→D11: el módulo migrate_upgrade genera automáticamente las migraciones para pasar de Drupal 7 a Drupal 11. Son dos cosas distintas: esta guía no cubre la ruta de upgrade, que será tema de otro artículo.

La Migrate API de Drupal funciona con un esquema simple: source (de dónde vienen los datos), process (cómo se transforman) y destination (a dónde van). Todo se define en ficheros YAML y se ejecuta con comandos drush.

Instalación

Necesitas tres módulos contrib que extienden el framework de migrate de Drupal core:

composer require drupal/migrate_plus:^6.0
composer require drupal/migrate_tools:^6.1
composer require drupal/migrate_devel:^3.0
MóduloVersiónDrupalFunción
migrate_plus^6.0^10.5 || ^11Source plugin url, parsers JSON/XML, entity_generate, entity_lookup
migrate_tools^6.1^9.1 || ^10 || ^11Comandos drush: status, import, rollback, messages, fields-source
migrate_devel^3.0^10 || ^11Depuración: —migrate-debug, plugin debug

Dentro de un módulo personalizado, creamos la carpeta migrations justo en la raíz del módulo. Drupal descubre automáticamente los ficheros YAML que encuentre ahí:

modules/
  mi_modulo/
    mi_modulo.info.yml
    mi_modulo.module
    migrations/
      noticias.json.yml
      noticias_es.yml

Cada fichero YAML corresponde a una migración. El identificador del fichero (id) es único dentro del sistema y es lo que usas en los comandos drush.

El ciclo de vida de una importación

Antes de meternos en el YAML, es importante entender las operaciones básicas que vamos a ejecutar con drush.

Ver el estado de las migraciones

drush migrate:status

Cada fichero YAML genera un registro en la tabla resultante:

 ------------------- ------------------ -------- ------- ---------- ------------- --------------------- 
 Group               Migration ID       Status   Total   Imported   Unprocessed   Last Imported        
 ------------------- ------------------ -------- ------- ---------- ------------- --------------------- 
 Default (default)   noticias_json      Idle     666     1          665           2025-07-16 14:46:36  
 Default (default)   noticias_json_es   Idle     666     0          666                                
 ------------------- ------------------ -------- ------- ---------- ------------- --------------------- 

Las columnas son: Migration ID (identificador del YAML), Status (Idle = listo, Importing = en progreso, Idle después de error si se quedó bloqueado), Total (registros en la fuente), Imported (ya importados), Unprocessed (pendientes) y Last Imported (última ejecución).

Los mensajes de error no aparecen aquí. Para verlos:

drush migrate:messages noticias_json

Ejecutar una importación

drush migrate:import noticias_json --limit=1 --migrate-debug
ParámetroFunción
--limit=1Número máximo de entidades a importar en esta ejecución
--migrate-debugSalida detallada: muestra source, destination y ID de cada fila (necesita migrate_devel)
--migrate-debug-preIgual pero antes de que se ejecute el process (no muestra el resultado del guardado)
--updateActualiza entidades ya importadas con los datos actuales de la fuente
--execute-dependenciesEjecuta primero las migraciones de las que depende esta

Si no pasas --limit, se importan todos los pendientes. Si la migración es grande, usa --limit para ir probando.

Revertir importaciones

Si algo ha salido mal o necesitas volver a empezar:

drush migrate:rollback noticias_json

El rollback solo borra lo importado por esa migración. Usa la tabla migrate_map_noticias_json para saber qué entidades creó. Cada registro de la tabla migrate_map_* almacena la relación entre el ID de la fuente y el ID de la entidad creada en Drupal. El rollback recorre esa tabla y borra las entidades correspondientes.

Lo que no borra son entidades que ya existían antes de la migración o que fueron creadas por otra migración. Si dos migraciones crean la misma entidad (mismo source ID), el rollback de una no afecta a la otra.

Resetear un estado bloqueado

Si la migración se queda en estado “Importing” (por un error, un timeout o un kill del proceso):

drush migrate:reset-status noticias_json

Esto devuelve el estado a Idle. Necesario antes de volver a ejecutar la importación.

Cadena de pruebas

En desarrollo, esta es la cadena que uso para probar cambios en el YAML:

drush migrate:reset-status noticias_json && \
drush migrate:rollback noticias_json && \
drush cr && \
drush migrate:import noticias_json --limit=1 --migrate-debug

Reset → Rollback → Cache clear → Import con una fila y depuración. Si la fila se importa correctamente, amplía el --limit o quítalo para procesar todo.

Anatomía de un fichero de migración

Un fichero YAML de migración tiene tres secciones principales: source, process y destination.

id: noticias_json
label: 'Migración de noticias desde JSON'
migration_group: default

source:

process:

destination:
  plugin: 'entity:node'
  • id: identificador único. Es lo que usas en drush. No tiene por qué coincidir con el nombre del fichero.
  • label: nombre legible para humanos.
  • migration_group: agrupación lógica. Puedes filtrar por grupo con --group en drush.
  • source: define de dónde y cómo vienen los datos.
  • process: el procesamiento del origen de datos a campos de Drupal.
  • destination: dónde se guardan los datos.

Drupal almacena una tabla de mapeo por cada migración: migrate_map_{id}. Esta tabla registra qué ID de la fuente corresponde con qué entidad de Drupal, y es la que permite el rollback y el --update.

Source: de JSON a filas

El source plugin url de migrate_plus permite obtener datos a través de una URL, sea remota o local. Para un fichero JSON local, la configuración es:

source:
  plugin: url
  data_fetcher_plugin: file
  urls:
    - public://import/noticias.json
  data_parser_plugin: json
  item_selector: ''
  fields:
    - name: id_contenido
      label: 'ID del contenido original'
      selector: ContentId
    - name: titulo
      label: 'Título'
      selector: title
    - name: body
      label: 'Body'
      selector: textNoticia
    - name: imagen_url
      label: 'Imagen principal'
      selector: imatgePrincipal
  ids:
    id_contenido:
      type: integer
ClaveFunción
plugin: urlSource plugin de migrate_plus para datos vía URL
data_fetcher_plugin: fileUsa el fetcher de ficheros locales (para URLs remotas usa http)
data_parser_plugin: jsonParsea la respuesta como JSON
item_selector: ''Array raíz: cada elemento del array principal es una fila. Si el JSON tiene estructura anidada, usa un selector como items o /data/results
fieldsDefine qué campos extraemos del JSON. name es la variable interna, selector es la ruta en el JSON (usa / para navegar la estructura)
idsIdentificador único de cada fila en la fuente. Drupal lo usa para relacionar fuentes con entidades. No necesitas crear un campo para esto: la tabla migrate_map_* lo gestiona automáticamente

Nota sobre item_selector: en migrate_plus ≥6.0.3, un item_selector vacío ('') significa “array raíz”. En versiones anteriores se usaba /. Si tu JSON tiene los datos dentro de una clave, usa esa clave como selector (por ejemplo, data o results).

Una vez ejecutas migrate:status, el Total corresponde al número de elementos que el parser encontró en la fuente.

Process: del dato al campo

La sección process mapea los datos del source a los campos de Drupal. Hay varios patrones:

Mapeo directo

El más sencillo: un campo de la fuente se guarda directamente en un campo de Drupal.

process:
  type:
    plugin: default_value
    default_value: article
  langcode:
    plugin: default_value
    default_value: es
  title: titulo
  body: body

type y langcode usan default_value porque no vienen de la fuente. title y body son mapeos directos: Drupal usa el plugin get implícitamente para copiar el valor.

Callback: funciones propias

El plugin callback permite llamar a cualquier función PHP y pasarle uno o más parámetros:

process:
  field_debug_text:
    plugin: callback
    callable: basename
    source: imagen_url

Esto ejecuta basename($imagen_url) y guarda el resultado en field_debug_text. Como callable puedes usar cualquier función PHP incluyendo funciones definidas en el .module de tu módulo.

Variables temporales

Puedes almacenar valores intermedios usando prefijo _. Estas variables no se guardan en campos de Drupal, sino que se usan como paso intermedio para otros plugins:

process:
  _temp_filename:
    plugin: callback
    callable: basename
    source: imagen_url
  _temp_destination:
    plugin: callback
    callable: mi_modulo_generar_ruta
    source: "@_temp_filename"

Las variables temporales se referencian con @ (por ejemplo, @_temp_filename). El prefijo _ le indica a Drupal que esto no es un campo destino real.

Pipeline: de URL a alias

Una de las funcionalidades más potentes de la Migrate API es el pipeline: encadenar varios plugins de proceso donde la salida de uno es la entrada del siguiente.

Ejemplo práctico: transformar una URL completa en un alias de Drupal:

process:
  path:
    - plugin: get
      source: canonical_url
    - plugin: callback
      callable: parse_url
    - plugin: extract
      index:
        - path
    - plugin: single_value

El valor va pasando por cada paso:

  1. get: extrae canonical_url de la fuente → "https://www.ejemplo.org/noticias/la-mi-noticia"
  2. callback: ejecuta parse_url() → devuelve un array con las partes de la URL
  3. extract: extrae el valor del índice path del array → ["/noticias/la-mi-noticia"]
  4. single_value: convierte el array de un solo elemento en un valor simple → "/noticias/la-mi-noticia"

Los plugins de este pipeline son todos de Drupal core: get, callback, extract y single_value.

Imágenes: pipeline completo

Migrar imágenes es uno de los escenarios más complejos porque implica tres pasos: descargar el fichero, crear la entidad File y crear la entidad Media que referencia al fichero.

1. Descargar el fichero

El plugin download (de Drupal core) descarga un fichero desde una URL y lo guarda en el sistema de ficheros de Drupal. Necesita dos parámetros: la URL de origen y la ruta de destino.

process:
  _temp_filename:
    plugin: callback
    callable: basename
    source: imagen_url
  _temp_destination:
    plugin: callback
    callable: mi_modulo_generar_ruta
    source: "@_temp_filename"
  _temp_file_uri:
    plugin: download
    source:
      - imagen_url
      - '@_temp_destination'
    file_exists: rename
Opción file_existsComportamiento
replace (por defecto)Sobrecribe si ya existe
renameAñade _0, _1… hasta que el nombre sea único
use existingNo descarga nada y devuelve FALSE si ya existe

El plugin download devuelve la URI del fichero descargado (por ejemplo, public://import/foto.jpg), pero no crea la entidad File. Necesitas un paso adicional para eso.

2. Crear la entidad File

Aquí es donde tu módulo custom entra en juego. Necesitas una función que reciba la URI y devuelva el ID de la entidad File creada:

<?php

use Drupal\file\Entity\File;

/**
 * Crea una entidad File a partir de una URI y devuelve su ID.
 */
function mi_modulo_crear_entidad_archivo(string $file_uri): int {
  // File::create() no descarga nada: registra la URI como entidad.
  $file = File::create([
    'uri' => $file_uri,
    'status' => 1, // Publicado.
  ]);
  $file->save();

  // Añadir uso para que no se marque como huérfana.
  \Drupal::service('file.usage')
    ->add($file, 'mi_modulo', 'migration', 1);

  return $file->id();
}

En el YAML, lo llamamos como callback:

  _temp_file_id:
    plugin: callback
    callable: mi_modulo_crear_entidad_archivo
    source: "@_temp_file_uri"

_temp_file_id ahora contiene el ID de la entidad File (por ejemplo, 42).

Alternativa oficial: en lugar de un callback custom, puedes usar destination: plugin: entity:file como destino de la migración. Esto crea la entidad File automáticamente. El approach con callback es útil cuando necesitas lógica adicional (validar tipo MIME, generar thumbnails, etc.).

3. Crear la entidad Media

Con el ID del fichero, creamos la entidad Media usando entity_generate:

  field_imatge_principal:
    plugin: entity_generate
    entity_type: media
    bundle: image
    bundle_key: bundle
    value_key: name
    source:
      - '@_temp_filename'
    values:
      field_media_image/target_id: '@_temp_file_id'
      field_media_image/alt: '@_temp_filename'
ClaveFunción
entity_type: mediaTipo de entidad a crear
bundle: imageBundle del Media (image, video, document…)
bundle_key: bundleCampo que almacena el bundle en la entidad media
value_key: nameCampo usado para buscar si ya existe (evita duplicados)
sourceValor a buscar en value_key
valuesCampos adicionales a setear en la entidad Media

entity_generate es un plugin de migrate_plus que extiende entity_lookup. Primero busca si ya existe una entidad Media con ese name. Si la encuentra, devuelve su ID. Si no, la crea con los valores especificados.

Importante: los tipos de entidad con bundles (nodes, media, taxonomy_terms) requieren bundle_key explícito. Para media, el bundle_key es bundle. Para taxonomy terms, es vid. Para nodes, es type.

Listas y relaciones

sub_process para listas

Cuando el JSON contiene arrays (listas de elementos), el plugin sub_process itera sobre cada elemento:

process:
  field_recursos:
    plugin: sub_process
    source: recursos
    process:
      title: Caption
      uri: Link
      'options/attributes/target':
        plugin: static_map
        source: NewWindow
        default_value: ''
        map:
          'true': '_blank'

Para cada elemento del array recursos, ejecuta el bloque process interior. static_map convierte el valor booleano NewWindow en _blank o cadena vacía.

migration_lookup para referencias

Cuando necesitas referenciar entidades creadas por otra migración, usa el plugin migration_lookup de migrate_plus:

process:
  field_autor:
    plugin: migration_lookup
    source: autor_id
    migration: migracion_autores
    no_stub: true

Esto busca en la tabla migrate_map_migracion_autores si el autor_id tiene una entidad asociada. Si la encuentra, devuelve el ID de Drupal. Si no, devuelve null (o crea un stub si no_stub es false).

Para que migration_lookup funcione, necesitas declarar la dependencia:

migration_dependencies:
  required:
    - migracion_autores

Drupal ejecutará primero migracion_autores antes de esta migración si usas --execute-dependencies.

Plugins de proceso personalizados

Cuando los plugins de core y migrate_plus no cubren tu caso, puedes crear los tuyos. Deben estar en src/Plugin/migrate/process/ dentro de tu módulo.

Ejemplo: un plugin que descarga una imagen desde una URL y devuelve el ID de la entidad Media creada:

<?php

namespace Drupal\mi_modulo\Plugin\migrate\process;

use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\media\Entity\Media;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
use Drupal\migrate_plus\Plugin\migrate\process\EntityLookup;
use GuzzleHttp\ClientInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Busca o crea una entidad Media desde una URL.
 *
 * @MigrateProcessPlugin(
 *   id = "mi_modulo_media_from_url"
 * )
 */
class MediaFromUrl extends EntityLookup implements ContainerFactoryPluginInterface {

  protected $fileSystem;
  protected $httpClient;

  public static function create(
    ContainerInterface $container,
    array $configuration,
    $plugin_id,
    $plugin_definition,
    MigrationInterface $migration = NULL
  ) {
    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition, $migration);
    $instance->fileSystem = $container->get('file_system');
    $instance->httpClient = $container->get('http_client');
    return $instance;
  }

  public function transform(
    $value,
    \Drupal\migrate\MigrateExecutableInterface $migrate_executable,
    Row $row,
    $destination_property
  ) {
    // Configurar la búsqueda de la entidad.
    $this->configuration['entity_type'] = 'media';
    $this->configuration['bundle'] = $this->configuration['bundle'] ?? 'image';
    $this->configuration['value_key'] = 'name';

    // Buscar si ya existe la entidad Media.
    $media_id = parent::transform($value, $migrate_executable, $row, $destination_property);

    if ($media_id) {
      return $media_id;
    }

    // No existe: crear la entidad.
    $entity = $this->entityTypeManager->getStorage('media')->create([
      'name' => $value,
      'bundle' => $this->configuration['bundle'],
      'field_media_image/target_id' => $this->descargarYCrearArchivo($value),
    ]);
    $entity->save();
    return $entity->id();
  }

  protected function descargarYCrearArchivo($url) {
    // Lógica de descarga y creación de File entity.
    // ...
  }

}

Los puntos clave del plugin:

  • @MigrateProcessPlugin: anotación con el id que usarás en el YAML.
  • create(): inyecta servicios del contenedor (file_system, http_client, etc.).
  • transform(): recibe el valor procesado ($value), el ejecutable de la migración, el row (toda la fila de datos) y el nombre del campo destino.
  • parent::transform(): llama a EntityLookup::transform(), que busca la entidad. Si existe, devuelve el ID. Si no, devuelve null.

En el YAML, lo usas así:

  field_imatge_principal:
    plugin: mi_modulo_media_from_url
    source: imagen_url
    bundle: image

Depuración

Ver errores de una migración

drush migrate:messages noticias_json

Los mensajes incluyen el source ID, el nivel de severidad y el texto del error. Si una migración falla en una fila, el proceso continúa con las siguientes (a menos que uses --stop-on-failure).

Depurar fila a fila

Con migrate_devel instalado:

drush migrate:import noticias_json --limit=1 --migrate-debug

La salida muestra: datos de la fuente (source), resultado del process pipeline (process) y el ID de la entidad creada en Drupal (destination). Si usas --migrate-debug-pre, ves el resultado antes de que se guarde en la base de datos.

Listar campos disponibles del source

drush migrate:fields-source noticias_json

Devuelve los campos que definiste en la sección fields del source, con su nombre interno y descripción. Muy útil cuando estás escribiendo el YAML y no recuerdas cómo se llama un campo.

Importación incremental con —update

Cuando la fuente cambia periódicamente (por ejemplo, un JSON que se actualiza cada día), usa --update para sincronizar sin duplicar:

drush migrate:import noticias_json --update

Drupal compara el source ID de cada fila con la tabla migrate_map_*. Si el ID ya existe, actualiza la entidad correspondiente. Si es nuevo, lo importa. Esto funciona gracias al high water mark (high_water_property en la configuración del source), que Drupal usa para saber dónde se quedó la última vez.

Tablas de debug

Cada migración crea dos tablas en la base de datos:

TablaFunción
migrate_map_{id}Relación source ID → entity ID. Controla importación, rollback y update.
migrate_message_{id}Mensajes de error, warning e info generados durante la importación.

Puedes consultarlas directamente con SQL para diagnosticar problemas:

-- Ver entidades importadas por una migración
SELECT * FROM migrate_map_noticias_json;

-- Ver errores
SELECT * FROM migrate_message_noticias_json;

Lecciones aprendidas

  • Prueba con --limit=1 antes de importar todo. El 90% de los errores de migración aparecen en la primera fila: un campo mal nombrado, un selector JSON incorrecto, un campo requerido sin valor. Importa una fila, verifica que el nodo se creó correctamente en Drupal, y luego amplía.

  • Las variables temporales con _ son tu mejor amigo. Te permiten encadenar transformaciones complejas (descarga + creación de entidad + entity_generate) sin ensuciar los campos de destino. Piensa en ellas como el equivalent a las variables intermedias en un programa.

  • No te saltes el bundle_key en entity_generate. Sin bundle_key, entity_lookup no sabe en qué bundle buscar y puede encontrar duplicados en bundles equivocados o fallar directamente. Siempre fija entity_type, value_key, bundle_key y bundle.

  • Usa --migrate-debug con --limit=1, nunca con miles de registros. La salida de depuración es enorme. Si necesitas debuggear una fila concreta, usa --idlist en lugar de --limit.

  • El rollback no es una papelera de reciclaje. Borra entidades permanentemente. Si vas a hacer cambios grandes en el YAML, haz un backup de la base de datos antes de ejecutar migrate:rollback.


La Migrate API es una de las herramientas más potentes de Drupal para integración de datos. Configurar una migración bien estructurada te ahorra horas de trabajo manual y te da un sistema reproducible y depurable. Si estás pensando en sincronizar contenido desde una API externa o importar datos de un sistema legacy, merece la pena invertir tiempo en aprender bien source, process y destination.

Si necesitas gestionar colas de procesamiento asíncrono para acompañar las migraciones, tienes la guía de Queue API. Y si el rendimiento es tu prioridad, la guía de cache bins te explica cómo aislar la caché a bajo nivel.

Preguntas frecuentes

¿Qué es la Migrate API de Drupal?

La Migrate API es el framework nativo de Drupal para importar contenido desde sistemas externos: APIs REST, JSON, CSV o bases de datos heredadas. Gestiona el ciclo de vida completo de la importación con tres secciones definidas en YAML: source (de dónde vienen los datos), process (cómo se transforman) y destination (a dónde van). Incluye transformaciones, validaciones, control de errores y trazabilidad mediante tablas migrate_map.

¿Qué módulos necesito para importar JSON con la Migrate API?

Necesitas tres módulos contrib: `migrate_plus` (^6.0) que aporta el source plugin `url`, los parsers JSON/XML y los plugins entity_generate y entity_lookup; `migrate_tools` (^6.1) que añade los comandos drush de status, import, rollback y messages; y `migrate_devel` (^3.0) para depurar con `--migrate-debug`. Los ficheros YAML se colocan en la carpeta `migrations/` del módulo personalizado.

¿Cómo importar una migración de Drupal con drush?

La cadena de importación se ejecuta con `drush migrate:import id_migracion`. Para probar de forma segura usa `--limit=1` y `--migrate-debug`, que muestra el source y destination de cada fila. Para actualizar entidades ya importadas añade `--update`, y para ejecutar las dependencias usa `--execute-dependencies`. Antes de cada prueba conviene hacer reset-status, rollback y cache clear.

¿Cómo revertir o resetear una migración en Drupal?

`drush migrate:rollback id_migracion` borra solo las entidades creadas por esa migración, recorriendo la tabla `migrate_map_*`. No elimina entidades que ya existían antes ni las creadas por otras migraciones. Si la migración se queda bloqueada en estado Importing por un error o timeout, `drush migrate:reset-status id_migracion` la devuelve a estado Idle para poder reejecutarla.

¿Qué diferencia hay entre una migración custom y la ruta de upgrade D7 a D11?

Las migraciones custom, que cubre esta guía, importan datos desde una fuente externa (JSON, XML, CSV) con el plugin `url` de migrate_plus y ficheros YAML propios. La ruta de upgrade D7→D11 usa el módulo `migrate_upgrade`, que genera automáticamente las migraciones para pasar de Drupal 7 a Drupal 11. Son frameworks distintos y requieren enfoques diferentes.

¿Qué estructura tiene un fichero YAML de migración?

Un fichero YAML de migración tiene tres secciones principales: `source`, donde se define la fuente de datos y el parser (por ejemplo, un endpoint JSON con el plugin `url`); `process`, donde se mapean y transforman los campos con callbacks, variables temporales y plugins como sub_process, migration_lookup o entity_generate; y `destination`, que indica el tipo de entidad donde se guardará el resultado.