Extend the WordPress REST API with register_rest_field()

The WordPress REST API exposes a set of default fields for every post — id, title, content, status, and so on. When your theme or plugin stores additional data in post meta, taxonomies, or custom tables, you can surface that data in the API response without building a custom endpoint by using register_rest_field().

Problem: The WordPress REST API response for a post type is missing custom field values — such as ACF data or post meta — that a JavaScript front end or mobile app needs.

Solution: Call register_rest_field() inside a rest_api_init hook, specifying the post type, the field name as it should appear in the response, and a get_callback that returns the value. The field then appears at the top level of every REST API response for that object type.

register_rest_field() accepts three arguments: the post type (or array of types), the field name that will appear in the JSON, and an array of callbacks — one to read the value (get_callback), one to write it (update_callback), and an optional schema definition.

Example 1 — expose post meta in the REST response:

<?php
add_action( 'rest_api_init', 'register_post_meta_rest_field' );

function register_post_meta_rest_field() {
    register_rest_field(
        'post',           // post type
        'reading_time',   // key name in the JSON response
        [
            'get_callback'    => 'get_reading_time_meta',
            'update_callback' => 'update_reading_time_meta',
            'schema'          => [
                'description' => 'Estimated reading time in minutes.',
                'type'        => 'integer',
                'context'     => [ 'view', 'edit' ],
            ],
        ]
    );
}

function get_reading_time_meta( $post ) {
    return (int) get_post_meta( $post['id'], '_reading_time', true );
}

function update_reading_time_meta( $value, $post ) {
    update_post_meta( $post->ID, '_reading_time', (int) $value );
}

A GET request to /wp-json/wp/v2/posts/123 will now include "reading_time": 5 in the response. A PATCH request with a JSON body of {"reading_time": 8} will update the meta value.

Example 2 — add the field to a custom post type:

<?php
add_action( 'rest_api_init', 'register_product_sku_field' );

function register_product_sku_field() {
    register_rest_field(
        'product',   // custom post type slug
        'sku',
        [
            'get_callback' => function ( $post ) {
                return get_post_meta( $post['id'], '_sku', true );
            },
            'schema' => [
                'type'    => 'string',
                'context' => [ 'view' ],
            ],
        ]
    );
}

Example 3 — expose taxonomy terms as a flat array:

<?php
add_action( 'rest_api_init', 'register_tag_names_field' );

function register_tag_names_field() {
    register_rest_field(
        'post',
        'tag_names',
        [
            'get_callback' => function ( $post ) {
                $terms = get_the_terms( $post['id'], 'post_tag' );
                if ( is_wp_error( $terms ) || empty( $terms ) ) {
                    return [];
                }
                return wp_list_pluck( $terms, 'name' );
            },
            'schema' => [
                'type'  => 'array',
                'items' => [ 'type' => 'string' ],
            ],
        ]
    );
}

This is especially convenient when building a decoupled front end (React, Vue, etc.) that needs clean, flat data structures rather than navigating nested term objects.

NOTE: register_rest_field() adds the field to all REST contexts by default. If the field contains sensitive data, restrict the context in the schema to 'edit' only, and make sure the get_callback checks current_user_can() before returning privileged information.