How to create a JSON exporter for custom content in PHP in WordPress

Contents

Introduction

This article is a complete, detailed tutorial showing how to create a JSON exporter for custom content in WordPress using PHP. You will find practical code examples (for a REST endpoint, an admin export link, and a WP-CLI command), full explanations of options and edge cases, performance and security recommendations, and examples for exporting post fields, custom fields (meta), taxonomies, featured images and attachments, plus options for large-site exports and gzipped downloads. Copy the code snippets into a plugin or mu-plugin file, adapt the names and capabilities to your project, and deploy.

Prerequisites

  • WordPress 4.7 (REST API included) or newer.
  • Familiarity with creating a plugin or adding code to a mu-plugin.
  • Knowledge of PHP, WP_Query, and WordPress functions like get_post_meta(), wp_get_post_terms(), wp_get_attachment_url().
  • Access to wp-config.php and server settings if exporting very large datasets (memory, execution time).

Overview: approaches

You can provide JSON exports in three common ways — choose one or implement multiple for different consumers:

  • REST API endpoint — good for programmatic consumption, supports query parameters, pagination, and standard authentication (cookies, application passwords, OAuth, etc.).
  • Admin export page or admin_post handler — convenient for site admins to click a link and download a JSON file use nonces and capability checks.
  • WP-CLI command — best for very large sites or automation on the server, avoids web server timeouts and memory limits.

Designing the JSON schema

Before writing code, decide what data shape you need. A recommended generic export item for a post-like object:

  • ID, post_type, post_title, post_content, post_excerpt, post_status, post_date, post_modified
  • author (ID, user_login, display_name)
  • meta — associative array of custom fields (unserialized where appropriate)
  • terms — keyed by taxonomy, with term_id, name, slug, taxonomy
  • featured_image — ID and URL (optionally with metadata or base64 data if you want to embed files)
  • attachments — list of attachments with ID, mime type, url, metadata (optionally base64)

Minimal schema example (JSON)

{
  schema_version: 1.0,
  site_url: https://example.com,
  exported_at: 2025-09-26T12:34:56Z,
  data: [
    {
      ID: 123,
      post_type: product,
      post_title: My product,
      post_content: ...,
      post_date: 2025-01-01 12:00:00,
      author: {
        ID: 2,
        user_login: editor,
        display_name: Editor Name
      },
      meta: {
        _price: 29.99,
        sizes: [S,M,L]
      },
      terms: {
        product_cat: [
          {term_id: 5, name: T-shirts, slug: t-shirts, taxonomy: product_cat}
        ]
      },
      featured_image: {
        ID: 555,
        url: https://example.com/wp-content/uploads/2025/01/image.jpg
      }
    }
  ]
}

Export via REST API: full example

This REST-based exporter is flexible and supports query parameters, pagination, and include/exclude flags for meta, terms, and attachments. Add the code to a plugin file (e.g., my-json-exporter.php) or a mu-plugin.

 GET,
        callback => my_export_rest_callback,
        permission_callback => function( request ) {
            // Customize permission: restrict to users who can export
            // For public exports remove or adjust this to your needs.
            return current_user_can(export)  current_user_can(manage_options)
        },
    ))
})

/
  REST callback: validate params, build query and return WP_REST_Response.
 
  Query parameters supported:
  - post_type (string) default: post
  - page (int) default: 1
  - per_page (int) default: 50 (limit on server recommended)
  - meta (bool) include postmeta - default true
  - terms (bool) include taxonomies - default true
  - attachments (bool) include attachments metadata - default false
 /
function my_export_rest_callback( WP_REST_Request request ) {
    params = request->get_params()

    post_type           = isset(params[post_type]) ? sanitize_text_field(params[post_type]) : post
    page                = max(1, (int) (params[page] ?? 1))
    per_page            = max(1, min(200, (int) (params[per_page] ?? 50))) // cap 200 for safety
    include_meta        = isset(params[meta]) ? filter_var(params[meta], FILTER_VALIDATE_BOOLEAN) : true
    include_terms       = isset(params[terms]) ? filter_var(params[terms], FILTER_VALIDATE_BOOLEAN) : true
    include_attachments = isset(params[attachments]) ? filter_var(params[attachments], FILTER_VALIDATE_BOOLEAN) : false

    query_args = array(
        post_type      => post_type,
        posts_per_page => per_page,
        paged          => page,
        post_status    => any,
        orderby        => ID,
        order          => ASC,
        fields         => all,
    )

    q = new WP_Query( query_args )

    items = array()
    foreach ( q->posts as post ) {
        items[] = my_export_prepare_post( post, include_meta, include_terms, include_attachments )
    }

    meta = array(
        total => (int) q->found_posts,
        pages => (int) ceil( q->found_posts / per_page ),
        page  => (int) page,
        per_page => (int) per_page,
    )

    payload = array(
        schema_version => 1.0,
        site_url       => get_site_url(),
        exported_at    => gmdate(c),
        data           => items,
        _paging        => meta,
    )

    return rest_ensure_response( payload )
}

/
  Prepare a single post for export: fields, meta, terms, attachments.
 /
function my_export_prepare_post( post, include_meta = true, include_terms = true, include_attachments = false ) {
    if ( is_int( post ) ) {
        post = get_post( post )
    }

    data = array(
        ID           => (int) post->ID,
        post_type    => post->post_type,
        post_title   => get_the_title( post ),
        post_content => post->post_content, // raw content if you want rendered apply the_content filters
        post_excerpt => post->post_excerpt,
        post_status  => post->post_status,
        post_date    => post->post_date,
        post_modified=> post->post_modified,
        author       => array(
            ID           => (int) post->post_author,
            user_login   => get_the_author_meta( user_login, post->post_author ),
            display_name => get_the_author_meta( display_name, post->post_author ),
        ),
    )

    // Meta
    if ( include_meta ) {
        raw_meta = get_post_meta( post->ID )
        meta = array()

        foreach ( raw_meta as meta_key => values ) {
            // Values is an array of values for this meta key.
            if ( count( values ) === 1 ) {
                meta[ meta_key ] = maybe_unserialize( values[0] )
            } else {
                meta[ meta_key ] = array_map( maybe_unserialize, values )
            }
        }
        data[meta] = meta
    }

    // Taxonomies / terms
    if ( include_terms ) {
        taxonomies = get_object_taxonomies( post->post_type )
        terms_out = array()

        foreach ( taxonomies as tax ) {
            terms = wp_get_post_terms( post->ID, tax, array( fields => all ) )
            if ( ! is_wp_error( terms )  ! empty( terms ) ) {
                terms_out[ tax ] = array_map( function( term ) {
                    return array(
                        term_id  => (int) term->term_id,
                        name     => term->name,
                        slug     => term->slug,
                        taxonomy => term->taxonomy,
                    )
                }, terms )
            }
        }
        if ( ! empty( terms_out ) ) {
            data[terms] = terms_out
        }
    }

    // Featured image  attachments
    if ( include_attachments ) {
        // Featured image
        thumb_id = get_post_thumbnail_id( post->ID )
        if ( thumb_id ) {
            data[featured_image] = array(
                ID  => (int) thumb_id,
                url => wp_get_attachment_url( thumb_id ),
            )
        }

        // All attachments attached to the post
        attachments = get_attached_media( , post->ID )
        att_out = array()
        foreach ( attachments as att ) {
            att_out[] = array(
                ID       => (int) att->ID,
                filename => wp_basename( get_attached_file( att->ID ) ),
                url      => wp_get_attachment_url( att->ID ),
                mime_type=> get_post_mime_type( att->ID ),
                metadata => wp_get_attachment_metadata( att->ID ),
            )
        }
        if ( ! empty( att_out ) ) {
            data[attachments] = att_out
        }
    }

    return data
}
?>

Notes about the REST approach

  • Use permission_callback to enforce authentication. For public read-only export you might return true for read-only data but be careful with meta and attachments that may contain secrets.
  • Limit posts_per_page to avoid memory exhaustion use pagination (page per_page) and the _paging response to iterate.
  • Use JSON serialization via the REST response — WordPress will set the Content-Type and encode JSON safely.

Admin export link (download JSON file)

Add an admin submenu link that generates and streams a download to the user. This example uses admin_post handler and nonce verification.

JSON Exporter

echo

Download JSON export

echo
} // Handle export request add_action(admin_post_my_json_export, my_json_export_handler) function my_json_export_handler() { // Verify nonce if ( ! isset( _GET[nonce] ) ! wp_verify_nonce( _GET[nonce], my_json_export ) ) { wp_die( Invalid nonce ) } if ( ! current_user_can( manage_options ) ) { wp_die( Insufficient permissions ) } // Build the data (example: export all post posts) posts = get_posts( array( post_type => post, post_status => any, numberposts => -1, ) ) items = array() foreach ( posts as post ) { items[] = my_export_prepare_post( post, true, true, false ) } payload = array( schema_version => 1.0, site_url => get_site_url(), exported_at => gmdate(c), data => items, ) filename = wp-export- . date(YmdHis) . .json header( Content-Type: application/json charset=utf-8 ) header( Content-Disposition: attachment filename= . filename . ) header( Expires: 0 ) header( Cache-Control: must-revalidate, post-check=0, pre-check=0 ) header( Pragma: public ) echo wp_json_encode( payload, JSON_PRETTY_PRINT JSON_UNESCAPED_SLASHES ) exit } ?>

Gzipped download variant

For very large exports consider creating a gzipped file and sending it with Content-Encoding or as a pre-created .gz file. The server must have enough disk space and memory. Example: use gzencode() on the JSON string and output it with header(Content-Encoding: gzip).


WP-CLI exporter (recommended for large sites)

WP-CLI avoids PHP-FPM timeouts and web server memory limits. Register a command in your plugin and run it from the shell.

 post_type,
                posts_per_page => per_page,
                paged          => page,
                post_status    => any,
            ) )

            foreach ( q->posts as post ) {
                results[] = my_export_prepare_post( post, true, true, false )
            }

            page  
        } while ( page <= ceil( q->found_posts / per_page ) )

        payload = array(
            schema_version => 1.0,
            site_url       => get_site_url(),
            exported_at    => gmdate(c),
            data           => results,
        )

        file_put_contents( outfile, wp_json_encode( payload, JSON_PRETTY_PRINT  JSON_UNESCAPED_SLASHES ) )
        WP_CLI::success( Wrote export to {outfile} )
    } )
}
?>

Including attachments as base64 (warning: huge files)

If you need to embed actual file contents in the JSON, you can base64-encode attachments. This will produce extremely large JSON files and can be slow. Only use for small binary files or limited sets.

// Example: inside my_export_prepare_post when include_attachments === true and you accept ?embed_files=1
file_path = get_attached_file( att->ID )
if ( file_exists( file_path )  filesize( file_path ) < 5  1024  1024 ) { // 5 MB safety
    data = file_get_contents( file_path )
    att_out_item[content_base64] = base64_encode( data )
}

Handling serialized and complex meta

WordPress stores arrays and objects as serialized strings in postmeta. When exporting, use maybe_unserialize() on meta values to restore arrays and objects to PHP arrays. Then json_encode will convert them to JSON arrays/objects. Be aware that some meta values may contain objects or classes that dont convert well — sanitize and normalize unexpected structures if necessary.

Exporting relationships and custom tables

If your custom content uses relationship tables (e.g., separate tables or post-to-post relationship tables), join those rows into the exported item. Always use wpdb->prepare() for manual SQL and never trust raw input. Example pattern:

global wpdb
rows = wpdb->get_results( wpdb->prepare(
    SELECT related_id, relation_meta FROM {wpdb->prefix}my_relation_table WHERE post_id = %d,
    post->ID
), ARRAY_A )

Pagination, memory and performance strategies

Example: streaming JSON array to file (low memory)

out = fopen( /path/to/export.json, w )
fwrite( out, {schema_version:1.0,site_url: . get_site_url() . ,exported_at: . gmdate(c) . ,data:[ )
first = true
paged = 1
per_page = 200
do {
    q = new WP_Query( array( post_type => post, posts_per_page => per_page, paged => paged, post_status => any ) )
    foreach ( q->posts as post ) {
        if ( ! first ) {
            fwrite( out, , )
        }
        fwrite( out, wp_json_encode( my_export_prepare_post( post, true, true, false ), JSON_UNESCAPED_SLASHES ) )
        first = false
    }
    paged  
} while ( paged <= ceil( q->found_posts / per_page ) )
fwrite( out, ]} )
fclose( out )

Security considerations

Example curl requests

Example using an application password for REST authentication:

curl --user username:application-password 
  -H Accept: application/json 
  https://example.com/wp-json/my-export/v1/posts?post_type=productper_page=100page=1 
  -o export-page-1.json

Fetch repeatedly for all pages and combine or import.

Parameters reference (REST endpoint)

post_type string - post type slug. Default post.
page int - page number for pagination. Default 1.
per_page int - number of posts per page. Server may cap it (example caps at 200).
meta bool - include custom fields/meta. Default true.
terms bool - include taxonomies/terms. Default true.
attachments bool - include attachment metadata (not base64). Default false.

Troubleshooting and common pitfalls

Example: complete simple REST consumer loop (bash)

Example script that downloads all pages and concatenates results client-side. This demonstrates client-side use of the _paging response.

# Example to fetch all pages using application password
SITE=https://example.com
USER=username
APP_PASS=abcd efgh ijkl mnop # use your application password
OUTPUT=export-full.json
PAGE=1
PER_PAGE=100
TMP_DIR=(mktemp -d)
while true do
  curl -s -u {USER}:{APP_PASS} 
    {SITE}/wp-json/my-export/v1/posts?post_type=postper_page={PER_PAGE}page={PAGE} 
    -o {TMP_DIR}/page-{PAGE}.json

  # Check if file has data
  if jq -e .data  length == 0 {TMP_DIR}/page-{PAGE}.json > /dev/null then
    break
  fi

  PAGE=((PAGE   1))
done

# Combine pages into one JSON array (jq required)
jq -s {
  schema_version: 1.0,
  site_url: input_filename,
  exported_at: now  todate,
  data: map(.data)  add
} {TMP_DIR}/page-.json > {OUTPUT}

rm -r {TMP_DIR}

Final recommendations and best practices

Sample useful links



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Leave a Reply

Your email address will not be published. Required fields are marked *