Extending Core Routes

Add fields to core endpoints with register_rest_field, expose meta and post types via show_in_rest, and filter rest_prepare_{$type}.

Syntaxregister_rest_field( string|array $object_type, string $attribute, array $args = array() )

Most of the time you do not need a new route — you need /wp/v2/posts to carry one more piece of data. There are three ways in, and they are not interchangeable.

show_in_rest

Registration arguments decide what exists at all. register_post_type() and register_taxonomy() take 'show_in_rest' => true (WordPress 4.7), plus rest_base, rest_controller_class, and rest_namespace since 5.9. register_post_meta() takes the same key, and the meta then appears inside the response's meta object. The shorthand 'show_in_rest' => true is equivalent to 'show_in_rest' => array() with a schema inferred from type; the longhand array( 'schema' => array( … ) ) is required for array and object meta (WordPress 5.3), where you must describe items or properties yourself.

register_rest_field()

Introduced in WordPress 4.7, this adds a top-level field to an object type — post, page, term, comment, user — with a get_callback, an optional update_callback and a schema. Call it on rest_api_init. Use it for derived or computed values that are not a stored meta key.

rest_prepare_{$post_type}

The filter rest_prepare_post, rest_prepare_page and so on (WordPress 4.7) receives ( WP_REST_Response $response, WP_Post $post, WP_REST_Request $request ) after the controller has built the response. Use it to remove or rewrite what core already produced — it is the only one of the three that can take something away.

Example

Example · php
<?php
add_action( 'init', 'acme_register_book_meta' );

function acme_register_book_meta() {
    // Shorthand: a scalar, single value. The REST schema is inferred from 'type'.
    register_post_meta(
        'book',
        'acme_isbn',
        array(
            'type'              => 'string',
            'description'       => __( 'ISBN-13.', 'acme' ),
            'single'            => true,
            'default'           => '',
            'show_in_rest'      => true,
            'sanitize_callback' => 'sanitize_text_field',
            // Runs during the edit_post_meta / add_post_meta / delete_post_meta
            // capability checks, so it gates writes only -- never reads. Left
            // out, it defaults to __return_true for an unprotected key and
            // __return_false for an _underscore-prefixed (protected) one.
            'auth_callback'     => function ( $allowed, $meta_key, $post_id ) {
                return current_user_can( 'edit_post', $post_id );
            },
        )
    );

    // Longhand: array meta MUST spell out the schema, including items.
    // Since WordPress 5.3. Omit it and register_post_meta() returns false.
    register_post_meta(
        'book',
        'acme_translators',
        array(
            'type'         => 'array',
            'single'       => true,
            'show_in_rest' => array(
                'schema' => array(
                    'type'  => 'array',
                    'items' => array( 'type' => 'string' ),
                ),
            ),
        )
    );
}

// Both appear under "meta" in GET /wp-json/wp/v2/books/<id>:
//   "meta": { "acme_isbn": "9780000000000", "acme_translators": [ "A. Smith" ] }
// The post type must have been registered with
//   'show_in_rest' => true, 'supports' => array( 'title', 'editor', 'custom-fields' )

When to use it

  • Exposing a custom post type and its meta so the block editor can save them.
  • Adding a computed field, such as reading time or a signed download URL, to /wp/v2/posts.
  • Removing fields a public JSON feed should not carry before it hits a CDN.
  • Letting a headless front end write a custom meta key with a normal POST to wp/v2.

More examples

A computed top-level field with register_rest_field()

Use register_rest_field for values you derive rather than store. Note the get_callback receives the prepared response data as an array, not a WP_Post, so reach for $post['id'] and then whatever getter you need. Supplying schema is what makes the field visible to _fields, to the OPTIONS request, and to the block editor's data store.

Example · php
<?php
add_action(
    'rest_api_init',
    function () {
        register_rest_field(
            'post',              // object type; also 'page', 'term', 'comment', 'user'
            'reading_time',      // becomes a top-level key in the response
            array(
                // ( array $post, string $field_name, WP_REST_Request $request, string $object_type )
                'get_callback'    => function ( $post, $field_name, $request ) {
                    $words = str_word_count( wp_strip_all_tags( get_post_field( 'post_content', $post['id'] ) ) );
                    return (int) ceil( $words / 200 );
                },
                // Read-only: with no update_callback, a value sent for this
                // field on write is silently skipped, not rejected with an error.
                'update_callback' => null,
                'schema'          => array(
                    'description' => __( 'Estimated reading time in minutes.', 'acme' ),
                    'type'        => 'integer',
                    'context'     => array( 'view', 'edit' ),
                    'readonly'    => true,
                ),
            )
        );
    }
);

Trimming a core response with rest_prepare_{$post_type}

This filter is the only one of the three approaches that can subtract. Reach for it to slim a public payload, to conditionally redact a field, or to correct something a core controller produced — but check the context parameter first, because the block editor reads the same route with context=edit and breaks if you remove fields it expects.

Example · php
<?php
/**
 * Hide the author's email-derived data and drop an unused block from the
 * payload of a public, cacheable posts endpoint.
 *
 * Parameters, in order: WP_REST_Response, WP_Post, WP_REST_Request. Since 4.7.
 */
add_filter(
    'rest_prepare_post',
    function ( $response, $post, $request ) {
        // Only touch the public read context; leave the editor's payload alone.
        if ( 'edit' === $request->get_param( 'context' ) ) {
            return $response;
        }

        $data = $response->get_data();
        unset( $data['guid'], $data['ping_status'], $data['to_ping'] );
        $response->set_data( $data );

        // _links are separate from the data array and are removed by relation.
        $response->remove_link( 'https://api.w.org/attachment' );

        return $response;
    },
    10,
    3
);

Discussion

  • Be the first to comment on this lesson.