Export/Import Guide¶
Overview¶
strapi-kit provides comprehensive export/import functionality for migrating Strapi content between instances. The system automatically handles relation resolution using content type schemas.
Quick Start¶
Export¶
from strapi_kit import SyncClient, StrapiExporter, StrapiConfig
config = StrapiConfig(base_url="http://localhost:1337", api_token="token")
with SyncClient(config) as client:
exporter = StrapiExporter(client)
# Export content types. Default document_status=DRAFT (v5 status=draft).
# Pass document_status=None for published-only.
# include_media defaults to True and then requires media_dir.
export_data = exporter.export_content_types(
["api::article.article", "api::author.author"],
include_media=False,
)
# Save to file
exporter.save_to_file(export_data, "export.json")
Import¶
from strapi_kit import StrapiImporter
target_config = StrapiConfig(base_url="http://localhost:1338", api_token="token")
with SyncClient(target_config) as client:
importer = StrapiImporter(client)
# Load and import
export_data = StrapiExporter.load_from_file("export.json")
result = importer.import_data(export_data)
print(f"Imported {result.entities_imported} entities")
Relation Resolution¶
How It Works¶
Relations are automatically resolved using content type schemas:
- During Export: Schemas are fetched from the Content-Type Builder API
- Schema Storage: Schemas are included in the export metadata
- During Import: Relations are resolved by looking up target content types from schemas.
Component schemas walked at export are stored in
metadata.component_schemasso nested paths such asseo[0].authorresolve without a destination Content-Type Builder fetch.
Example: When importing an article with {"author": ["auth-doc"]}, the
system:
1. Looks up the schema to find that author targets "api::author.author"
2. Maps the source documentId to the destination documentId
3. PUTs {"data": {"author": "new-author-doc"}} via relation_write()
Relation writes cover top-level fields and nested component / dynamic-zone
paths such as seo[0].author. Nested writes merge dest documentIds into
a copy of the exported component object so scalar component fields are
kept. Paths that cannot be applied (missing component shell) are import
errors (success=False). On dry-run those nested skips are warnings and
do not flip success. Per-target dest-resolution misses are attached to
result.unresolved_relations / result.relations_unresolved. A field
that resolved only a subset of IDs is not written. On dry-run, success
is write-safety; check relations_unresolved. On live import, each dest
miss is an error and flips success.
On a v4 destination (create returns no documentId), import falls back to
numeric build_nested_numeric_payload / PUT {endpoint}/{new_id}.
build_relation_payload remains a public helper.
RelationId (StrictInt | StrictStr) is a Pydantic input type on
ExportedEntity.relations and UnresolvedRelation.old_id. It is not an
isinstance target. Extract helpers accept int / str and reject
bool.
Media fields are converted to a dest write id (documentId when the
upload recorded one, otherwise the remapped numeric id). Populate blobs
(mime, url, source documentId) are not posted.
locale=* export yields one row per locale with the same documentId
(locale=all is a fallback when * is rejected).
Import keys existence and writes by (documentId, locale). The first locale
of a source document creates (or updates/skips that locale). Later locales
of the same source documentId write PUT {destDoc}?locale=. Relation
updates and publish pass the row locale. ConflictResolution.SKIP is
per-locale: a missing locale is written even when another locale of the
same document already exists.
Schema Structure¶
Schemas include field metadata for relation resolution:
{
"uid": "api::article.article",
"pluralName": "articles",
"fields": {
"author": {
"type": "relation",
"relation": "manyToOne",
"target": "api::author.author"
}
}
}
Export Options¶
Basic Export¶
export_data = exporter.export_content_types(
["api::article.article", "api::author.author"],
include_media=False,
)
Schemas are always included for relation resolution. Completeness
defaults to document_status=DocumentStatus.DRAFT (v5 status=draft,
confirmed v4 publicationState=preview). Pass document_status=None
for published-only.
Import conflict detection follows that default: a published miss is
retried with status=draft so a draft-only re-import does not create a
second document.
Relation and media extraction accept both the v4 { "data": ... }
wrapper and flat Strapi 5 populate=* objects (documentId / mime
at the field root). Import writes relations with relation_write()
(documentId strings). Export/import require pluralName from the
content-type schema and do not invent a path from the UID.
Export with Media¶
export_data = exporter.export_content_types(
["api::article.article"],
include_media=True,
media_dir="export/media"
)
Progress Tracking¶
def progress_callback(current, total, message):
print(f"[{current}/{total}] {message}")
export_data = exporter.export_content_types(
["api::article.article"],
progress_callback=progress_callback
)
Import Options¶
Basic Import¶
Import Options¶
from strapi_kit.models import ImportOptions, ConflictResolution
options = ImportOptions(
skip_relations=False, # Import relations (default)
import_media=True, # Import media files
conflict_resolution=ConflictResolution.SKIP, # Skip conflicts
dry_run=False # Actually perform import
)
result = importer.import_data(export_data, options)
Conflict Resolution Strategies¶
Conflicts are per (documentId, locale). A missing locale of an existing
document is not a conflict.
SKIP: Skip locales that already exist; write missing localesUPDATE: Overwrite existing locales; write missing localesFAIL: Finish the whole entity/relation/publish pass, including later rows. Write missing locales. Do not overwrite existing locale fields or their outbound relations. Then raiseImportExportError(import-level failure, not fail-fast on the first hit). The exceptiondetailsinclude what already landed (entities_imported,relations_imported,errors,relations_unresolved). Dry-run still probes(documentId, locale)and raises; it does not write.
JSONL export¶
export_to_jsonl writes metadata first, then entities, then a media
manifest. After the stream it rewrites line 1 with real
total_entities / total_media (sibling temp copy, O(1) memory).
None means unknown; 0 means empty. Finished
export_content_types / JSON export snapshots both counts (0 when
there is no media). Import recounts when either field is None or
0 so older files that still write 0 keep working.
Working with Relations¶
Ensuring Complete Exports¶
To ensure all relations are resolved, include all related content types:
# Include all related content types
export_data = exporter.export_content_types([
"api::article.article",
"api::author.author", # Referenced by articles
"api::category.category" # Referenced by articles
])
Checking Import Results¶
result = importer.import_data(export_data)
# Check results
print(f"Success: {result.success}")
print(f"Entities imported: {result.entities_imported}")
print(f"Entities skipped: {result.entities_skipped}")
print(f"Unresolved dest relations: {result.relations_unresolved}")
# View ID mapping
for content_type, mapping in result.id_mapping.items():
print(f"{content_type}:")
for old_id, new_id in mapping.items():
print(f" {old_id} -> {new_id}")
# Check for warnings/errors
for warning in result.warnings:
print(f"Warning: {warning}")
for error in result.errors:
print(f"Error: {error}")
Inspecting Schemas¶
You can inspect schemas in export data:
# Load export
export_data = StrapiExporter.load_from_file("export.json")
# Inspect schemas
for content_type, schema in export_data.metadata.schemas.items():
print(f"\n{content_type}:")
print(f" Display Name: {schema.display_name}")
# Show relations
for field_name, field in schema.fields.items():
if field.type == "relation":
print(f" Relation: {field_name} -> {field.target}")
Direct Schema Cache Usage¶
The schema cache can be used directly:
from strapi_kit.cache import InMemorySchemaCache
with SyncClient(config) as client:
cache = InMemorySchemaCache(client)
# Get schema
schema = cache.get_schema("api::article.article")
# Check relation targets
target = schema.get_field_target("author")
print(target) # "api::author.author"
# Check if field is a relation
is_rel = schema.is_relation_field("author")
print(is_rel) # True
Troubleshooting¶
Missing Relations¶
Issue: "No ID mapping for X" warning
Cause: A relation references a content type not included in the export
Solution: Include all related content types:
export_data = exporter.export_content_types([
"api::article.article",
"api::author.author", # Add missing content types
])
Unresolved IDs¶
Issue: "Could not resolve X ID Y for field Z"
On live import this is an error (success=False) and a row on
result.unresolved_relations. On dry-run it is a warning; success
stays write-safe — check relations_unresolved.
Cause: A specific dest-relation target did not map (the entity was omitted from the export, or the dest never received a mapping)
Solutions:
1. Ensure all entities are exported (check filters)
2. Import the missing entities first
3. Create the missing entities manually in the target instance
4. Review result.unresolved_relations for the field, source id, and
target type
Schema Fetch Failures¶
Issue: ImportExportError: Schema with pluralName is required to export …
Cause: Content-Type Builder is unreachable, the UID is wrong, or the
schema has no pluralName (export/import will not invent a path from
the UID).
Solutions:
1. Verify the content type UID
2. Check the API token can read Content-Type Builder
3. Confirm the schema includes pluralName
Performance Considerations¶
- Export: +1 API call per content type (schema fetch)
- Import: No additional API calls (schemas loaded from export)
- Memory: ~10KB per content type schema (typical)
- Cache: In-memory only, cleared after operation
Best Practices¶
- Export Complete Sets: Always export related content types together
- Test First: Use
dry_run=Trueto validate imports. Dry-run counts missing dests as imported but does not map them to dest id0or the sourcedocumentId. It still reports unresolved dest relations as warnings (relations_unresolved) and countsentities_to_publish(live source rows this import would attempt to publish; SKIP/FAIL existing locales are not counted). Dry-runsuccessis write-safety, not “relations would apply.” Existing dests (SKIP/UPDATE, or a missing locale of an existing document) still map real dest ids. - Check Results: Always review warnings, errors, and
relations_unresolvedafter import - Media Handling: Download media files if needed for offline migration
- Version Compatibility: Ensure source and target Strapi versions are compatible
Schema Model Reference¶
from strapi_kit.models import (
ContentTypeSchema,
FieldSchema,
FieldType,
RelationType
)
# ContentTypeSchema
schema = ContentTypeSchema(
uid="api::article.article",
display_name="Article",
kind="collectionType",
plural_name="articles",
fields={...}
)
# Helper methods
target = schema.get_field_target("author")
is_rel = schema.is_relation_field("author")
Example: Complete Migration¶
from strapi_kit import SyncClient, StrapiConfig, StrapiExporter, StrapiImporter
from strapi_kit.models import ImportOptions
# Source instance
source_config = StrapiConfig(
base_url="http://localhost:1337",
api_token="source-token"
)
# Target instance
target_config = StrapiConfig(
base_url="http://localhost:1338",
api_token="target-token"
)
# Export from source
with SyncClient(source_config) as client:
exporter = StrapiExporter(client)
export_data = exporter.export_content_types(
[
"api::article.article",
"api::author.author",
"api::category.category",
],
include_media=False,
)
exporter.save_to_file(export_data, "migration.json")
# Import to target
with SyncClient(target_config) as client:
importer = StrapiImporter(client)
export_data = StrapiExporter.load_from_file("migration.json")
# Dry run first
result = importer.import_data(
export_data,
options=ImportOptions(dry_run=True)
)
if result.success:
# Actual import
result = importer.import_data(export_data)
print(f"Migration complete: {result.entities_imported} entities")