GDPR Art. 20 grants data subjects the right to receive data they have 'provided' in a structured, commonly used, machine-readable format. This is not satisfied by a PDF, an HTML page, or a JSON blob full of abbreviated field names like ts, uid, and evt. The portability right exists specifically so users can take their data to a competing service — which requires a format that a human or automated import tool can interpret without a technical manual. Failing to provide a self-documenting, versioned export also makes your own DSAR compliance fragile: if the schema changes and old exports are unreadable, you have no durable evidence trail.
Medium because the portability gap is a discrete Art. 20 violation but does not by itself put users at immediate harm — however, it compounds with Art. 15 (access) gaps when no export exists at all.
Ensure your export endpoint produces a self-documenting JSON structure with a _meta block, full field names, and a format version.
{
"_meta": {
"exportedAt": "2026-02-22T10:00:00Z",
"formatVersion": "1.0",
"schemaDescription": "Personal data export. Timestamps are UTC ISO 8601. IDs are UUIDs.",
"schemaUrl": "https://example.com/docs/data-export-schema"
},
"profile": {
"userId": "usr_abc123",
"emailAddress": "alice@example.com",
"displayName": "Alice",
"accountCreatedAt": "2025-06-01T09:00:00Z"
},
"contentCreated": [
{ "itemId": "item_xyz", "title": "My first post", "createdAt": "2025-07-15T14:30:00Z" }
]
}
Use full, descriptive field names throughout — never abbreviated database column names. Increment formatVersion whenever the schema changes so older exports remain interpretable. Include the _meta block in your existing app/api/user/export/route.ts response.
ID: gdpr-readiness.user-rights.right-to-portability
Severity: medium
What to look for: GDPR Article 20 grants the right to data portability — users must be able to receive data they have "provided" in a structured, commonly used, machine-readable format. Inspect any data export feature to determine: is the format JSON or CSV (machine-readable)? Is it a format a user could import into another service? Does the export include data the user actively provided (profile, uploaded content, created records) as distinct from inferred or derived data (portability covers provided data, not derived analytics)? Is there a self-documenting schema — field names that are descriptive without requiring a technical manual? Is a format version included so the schema can evolve without breaking existing exports? Count all instances found and enumerate each.
Pass criteria: Data export produces a structured JSON or CSV file. The format is self-documenting (descriptive field names, a _meta block with format version and description). The export includes all user-provided data. A schema URL or description is included or accessible. A user could reasonably understand and use the export without technical assistance. At least 1 implementation must be confirmed.
Fail criteria: No export feature. Export produces a proprietary, binary, or non-machine-readable format. JSON export uses abbreviated or opaque field names. No schema documentation provided. Export only returns raw database rows without structure or context.
Skip (N/A) when: Application has no data export feature — in which case the right-to-access check above already captures the gap. Mark as skip and note the dependency.
Cross-reference: The right-to-access check verifies the broader access mechanism that this portability export extends.
Detail on fail: Example: "No data export feature exists. Users cannot exercise their right to data portability." or "Export produces a .dat binary file with no schema documentation." or "JSON export uses abbreviated field names (ts, uid, evt) with no schema or description.".
Remediation: Ensure the export from your right-to-access endpoint is structured and self-documenting:
{
"_meta": {
"exportedAt": "2026-02-22T10:00:00Z",
"formatVersion": "1.0",
"schemaDescription": "Personal data export. Timestamps are UTC ISO 8601. IDs are UUIDs.",
"schemaUrl": "https://example.com/docs/data-export-schema"
},
"profile": {
"userId": "usr_abc123",
"emailAddress": "alice@example.com",
"displayName": "Alice",
"accountCreatedAt": "2025-06-01T09:00:00Z"
},
"contentCreated": [
{
"itemId": "item_xyz",
"title": "My first post",
"body": "...",
"createdAt": "2025-07-15T14:30:00Z"
}
],
"purchases": [
{
"orderId": "ord_123",
"totalAmountCents": 2900,
"currency": "usd",
"purchasedAt": "2026-01-10T09:00:00Z"
}
]
}
Use full, descriptive field names. Include the _meta block. If your export format ever changes, increment formatVersion so older exports remain interpretable.