<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[58831] trunk: REST API, Meta: Store updates in database when they are equal to the defaults.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/58831">58831</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/58831","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>dmsnell</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2024-07-29 18:47:21 +0000 (Mon, 29 Jul 2024)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>REST API, Meta: Store updates in database when they are equal to the defaults.

This patch fixes an oversight from when default metadata values were introduced
in <a href="https://core.trac.wordpress.org/ticket/43941">#43941</a> in WordPress 5.5: metadata updates should persist in the database
even if they match the registered default value (because the default values 
can change over time).

Previously, the REST API code was comparing updated values against the value
returned by the default-aware `get_metadata()` method. This meant that if no
value existed in the database, and the default value was supplied to the update,
WordPress would think that the updated value was already persisted and skip
the database call.

Now, the `get_metadata_raw()` method is called for comparing whether or not
a database update is required, fixing the bug.

In this patch both issues are resolved.

Developed in https://github.com/wordpress/wordpress-develop/pull/6782
Discussed in https://core.trac.wordpress.org/ticket/55600

Follow-up to <a href="https://core.trac.wordpress.org/changeset/48402">[48402]</a>.

Props: dmsnell, kraftner, ramon-fincken.
Fixes <a href="https://core.trac.wordpress.org/ticket/55600">#55600</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludesrestapifieldsclasswprestmetafieldsphp">trunk/src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php</a></li>
<li><a href="#trunktestsphpunittestsrestapirestpostmetafieldsphp">trunk/tests/phpunit/tests/rest-api/rest-post-meta-fields.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludesrestapifieldsclasswprestmetafieldsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php       2024-07-29 18:20:11 UTC (rev 58830)
+++ trunk/src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php 2024-07-29 18:47:21 UTC (rev 58831)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -268,6 +268,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * Alters the list of values in the database to match the list of provided values.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.7.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 6.7.0 Stores values into DB even if provided registered default value.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param int    $object_id Object ID to update.
</span><span class="cx" style="display: block; padding: 0 10px">         * @param string $meta_key  Key for the custom field.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -290,7 +291,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        );
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $current_values = get_metadata( $meta_type, $object_id, $meta_key, false );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $current_values = get_metadata_raw( $meta_type, $object_id, $meta_key, false );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $subtype        = get_object_subtype( $meta_type, $object_id );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                if ( ! is_array( $current_values ) ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -367,6 +368,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * Updates a meta value for an object.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.7.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 6.7.0 Stores values into DB even if provided registered default value.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param int    $object_id Object ID to update.
</span><span class="cx" style="display: block; padding: 0 10px">         * @param string $meta_key  Key for the custom field.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -378,7 +380,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $meta_type = $this->get_meta_type();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Do the exact same check for a duplicate value as in update_metadata() to avoid update_metadata() returning false.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $old_value = get_metadata( $meta_type, $object_id, $meta_key );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $old_value = get_metadata_raw( $meta_type, $object_id, $meta_key );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $subtype   = get_object_subtype( $meta_type, $object_id );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                if ( is_array( $old_value ) && 1 === count( $old_value )
</span></span></pre></div>
<a id="trunktestsphpunittestsrestapirestpostmetafieldsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/rest-api/rest-post-meta-fields.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/rest-api/rest-post-meta-fields.php      2024-07-29 18:20:11 UTC (rev 58830)
+++ trunk/tests/phpunit/tests/rest-api/rest-post-meta-fields.php        2024-07-29 18:47:21 UTC (rev 58831)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3096,6 +3096,464 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Ensures that REST API calls with post meta containing the default value for the
+        * registered meta field stores the default value into the database.
+        *
+        * When the default value isn't persisted in the database, a read of the post meta
+        * at some point in the future might return a different value if the code setting the
+        * default changed. This ensures that once a value is intentionally saved into the
+        * database that it will remain durably in future reads.
+        *
+        * @ticket 55600
+        *
+        * @dataProvider data_scalar_default_values
+        *
+        * @param string $type              Scalar type of default value: one of `boolean`, `integer`, `number`, or `string`.
+        * @param mixed  $default_value     Appropriate default value for given type.
+        * @param mixed  $alternative_value Ignored in this test.
+        */
+       public function test_scalar_singular_default_is_saved_to_db( $type, $default_value, $alternative_value ) {
+               $this->grant_write_permission();
+
+               $meta_key_single = "with_{$type}_default";
+
+               register_post_meta(
+                       'post',
+                       $meta_key_single,
+                       array(
+                               'type'         => $type,
+                               'single'       => true,
+                               'show_in_rest' => true,
+                               'default'      => $default_value,
+                       )
+               );
+
+               $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) );
+               $request->set_body_params(
+                       array(
+                               'meta' => array(
+                                       $meta_key_single => $default_value,
+                               ),
+                       )
+               );
+
+               $response = rest_get_server()->dispatch( $request );
+               $this->assertSame(
+                       200,
+                       $response->get_status(),
+                       "API call should have returned successfully but didn't: check test setup."
+               );
+
+               $this->assertSame(
+                       array( (string) $default_value ),
+                       get_metadata_raw( 'post', self::$post_id, $meta_key_single, false ),
+                       'Should have stored a single meta value with string-cast version of default value.'
+               );
+       }
+
+       /**
+        * Ensures that REST API calls with multi post meta values (containing the default)
+        * for the registered meta field stores the default value into the database.
+        *
+        * When the default value isn't persisted in the database, a read of the post meta
+        * at some point in the future might return a different value if the code setting the
+        * default changed. This ensures that once a value is intentionally saved into the
+        * database that it will remain durably in future reads.
+        *
+        * Further, the total count of stored values may be wrong if the default value
+        * is culled from the results of a "multi" read.
+        *
+        * @ticket 55600
+        *
+        * @dataProvider data_scalar_default_values
+        *
+        * @param string $type              Scalar type of default value: one of `boolean`, `integer`, `number`, or `string`.
+        * @param mixed  $default_value     Appropriate default value for given type.
+        * @param mixed  $alternative_value Appropriate value for given type that doesn't match the default value.
+        */
+       public function test_scalar_multi_default_is_saved_to_db( $type, $default_value, $alternative_value ) {
+               $this->grant_write_permission();
+
+               $meta_key_multiple = "with_multi_{$type}_default";
+
+               // Register non-singular post meta for type.
+               register_post_meta(
+                       'post',
+                       $meta_key_multiple,
+                       array(
+                               'type'         => $type,
+                               'single'       => false,
+                               'show_in_rest' => true,
+                               'default'      => $default_value,
+                       )
+               );
+
+               // Write the default value as the sole value.
+               $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) );
+               $request->set_body_params(
+                       array(
+                               'meta' => array(
+                                       $meta_key_multiple => array( $default_value ),
+                               ),
+                       )
+               );
+
+               $response = rest_get_server()->dispatch( $request );
+               $this->assertSame(
+                       200,
+                       $response->get_status(),
+                       "API call should have returned successfully but didn't: check test setup."
+               );
+
+               $this->assertSame(
+                       array( (string) $default_value ),
+                       get_metadata_raw( 'post', self::$post_id, $meta_key_multiple, false ),
+                       'Should have stored a single meta value with string-cast version of default value.'
+               );
+
+               // Write multiple values, including the default, to ensure it remains.
+               $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) );
+               $request->set_body_params(
+                       array(
+                               'meta' => array(
+                                       $meta_key_multiple => array(
+                                               $default_value,
+                                               $alternative_value,
+                                       ),
+                               ),
+                       )
+               );
+
+               $response = rest_get_server()->dispatch( $request );
+               $this->assertSame(
+                       200,
+                       $response->get_status(),
+                       "API call should have returned successfully but didn't: check test setup."
+               );
+
+               $this->assertSame(
+                       array( (string) $default_value, (string) $alternative_value ),
+                       get_metadata_raw( 'post', self::$post_id, $meta_key_multiple, false ),
+                       'Should have stored both the default and non-default string-cast values.'
+               );
+       }
+
+       /**
+        * Ensures that REST API calls with post meta containing an object as the default
+        * value for the registered meta field stores the default value into the database.
+        *
+        * When the default value isn't persisted in the database, a read of the post meta
+        * at some point in the future might return a different value if the code setting the
+        * default changed. This ensures that once a value is intentionally saved into the
+        * database that it will remain durably in future reads.
+        *
+        * @ticket 55600
+        *
+        * @dataProvider data_scalar_default_values
+        *
+        * @param string $type              Scalar type of default value: one of `boolean`, `integer`, `number`, or `string`.
+        * @param mixed  $default_value     Appropriate default value for given type.
+        * @param mixed  $alternative_value Ignored in this test.
+        */
+       public function test_object_singular_default_is_saved_to_db( $type, $default_value, $alternative_value ) {
+               $this->grant_write_permission();
+
+               $meta_key_single = "with_{$type}_default";
+
+               // Register singular post meta for type.
+               register_post_meta(
+                       'post',
+                       $meta_key_single,
+                       array(
+                               'type'         => 'object',
+                               'single'       => true,
+                               'show_in_rest' => array(
+                                       'schema' => array(
+                                               'type'       => 'object',
+                                               'properties' => array(
+                                                       $type => array( 'type' => $type ),
+                                               ),
+                                       ),
+                               ),
+                               'default'      => (object) array( $type => $default_value ),
+                       )
+               );
+
+               $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) );
+               $request->set_body_params(
+                       array(
+                               'meta' => array(
+                                       $meta_key_single => (object) array( $type => $default_value ),
+                               ),
+                       )
+               );
+
+               $response = rest_get_server()->dispatch( $request );
+               $this->assertSame(
+                       200,
+                       $response->get_status(),
+                       "API call should have returned successfully but didn't: check test setup."
+               );
+
+               // Objects stored into the database are read back as arrays.
+               $this->assertSame(
+                       array( array( $type => $default_value ) ),
+                       get_metadata_raw( 'post', self::$post_id, $meta_key_single, false ),
+                       'Should have stored a single meta value with an object representing the default value.'
+               );
+       }
+
+       /**
+        * Ensures that REST API calls with multi post meta values (containing an object as
+        * the default) for the registered meta field stores the default value into the database.
+        *
+        * When the default value isn't persisted in the database, a read of the post meta
+        * at some point in the future might return a different value if the code setting the
+        * default changed. This ensures that once a value is intentionally saved into the
+        * database that it will remain durably in future reads.
+        *
+        * Further, the total count of stored values may be wrong if the default value
+        * is culled from the results of a "multi" read.
+        *
+        * @ticket 55600
+        *
+        * @dataProvider data_scalar_default_values
+        *
+        * @param string $type              Scalar type of default value: one of `boolean`, `integer`, `number`, or `string`.
+        * @param mixed  $default_value     Appropriate default value for given type.
+        * @param mixed  $alternative_value Appropriate value for given type that doesn't match the default value.
+        */
+       public function test_object_multi_default_is_saved_to_db( $type, $default_value, $alternative_value ) {
+               $this->grant_write_permission();
+
+               $meta_key_multiple = "with_multi_{$type}_default";
+
+               // Register non-singular post meta for type.
+               register_post_meta(
+                       'post',
+                       $meta_key_multiple,
+                       array(
+                               'type'         => 'object',
+                               'single'       => false,
+                               'show_in_rest' => array(
+                                       'schema' => array(
+                                               'type'       => 'object',
+                                               'properties' => array(
+                                                       $type => array( 'type' => $type ),
+                                               ),
+                                       ),
+                               ),
+                               'default'      => (object) array( $type => $default_value ),
+                       )
+               );
+
+               $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) );
+               $request->set_body_params(
+                       array(
+                               'meta' => array(
+                                       $meta_key_multiple => array( (object) array( $type => $default_value ) ),
+                               ),
+                       )
+               );
+
+               $response = rest_get_server()->dispatch( $request );
+               $this->assertSame(
+                       200,
+                       $response->get_status(),
+                       "API call should have returned successfully but didn't: check test setup."
+               );
+
+               // Objects stored into the database are read back as arrays.
+               $this->assertSame(
+                       array( array( $type => $default_value ) ),
+                       get_metadata_raw( 'post', self::$post_id, $meta_key_multiple, false ),
+                       'Should have stored a single meta value with an object representing the default value.'
+               );
+
+               $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) );
+               $request->set_body_params(
+                       array(
+                               'meta' => array(
+                                       $meta_key_multiple => array(
+                                               (object) array( $type => $default_value ),
+                                               (object) array( $type => $alternative_value ),
+                                       ),
+                               ),
+                       )
+               );
+
+               $response = rest_get_server()->dispatch( $request );
+               $this->assertSame(
+                       200,
+                       $response->get_status(),
+                       "API call should have returned successfully but didn't: check test setup."
+               );
+
+               // Objects stored into the database are read back as arrays.
+               $this->assertSame(
+                       array( array( $type => $default_value ), array( $type => $alternative_value ) ),
+                       get_metadata_raw( 'post', self::$post_id, $meta_key_multiple, false ),
+                       'Should have stored a single meta value with an object representing the default value.'
+               );
+       }
+
+       /**
+        * Ensures that REST API calls with post meta containing a list array as the default
+        * value for the registered meta field stores the default value into the database.
+        *
+        * When the default value isn't persisted in the database, a read of the post meta
+        * at some point in the future might return a different value if the code setting the
+        * default changed. This ensures that once a value is intentionally saved into the
+        * database that it will remain durably in future reads.
+        *
+        * @ticket 55600
+        *
+        * @dataProvider data_scalar_default_values
+        *
+        * @param string $type              Scalar type of default value: one of `boolean`, `integer`, `number`, or `string`.
+        * @param mixed  $default_value     Appropriate default value for given type.
+        * @param mixed  $alternative_value Ignored in this test.
+        */
+       public function test_array_singular_default_is_saved_to_db( $type, $default_value, $alternative_value ) {
+               $this->grant_write_permission();
+
+               $meta_key_single = "with_{$type}_default";
+
+               // Register singular post meta for type.
+               register_post_meta(
+                       'post',
+                       $meta_key_single,
+                       array(
+                               'type'         => 'array',
+                               'single'       => true,
+                               'show_in_rest' => array(
+                                       'schema' => array(
+                                               'type'  => 'array',
+                                               'items' => array(
+                                                       'type' => $type,
+                                               ),
+                                       ),
+                               ),
+                               'default'      => $default_value,
+                       )
+               );
+
+               $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) );
+               $request->set_body_params(
+                       array(
+                               'meta' => array(
+                                       $meta_key_single => array( $default_value ),
+                               ),
+                       )
+               );
+
+               $response = rest_get_server()->dispatch( $request );
+               $this->assertSame(
+                       200,
+                       $response->get_status(),
+                       "API call should have returned successfully but didn't: check test setup."
+               );
+
+               $this->assertSame(
+                       array( array( $default_value ) ),
+                       get_metadata_raw( 'post', self::$post_id, $meta_key_single, false ),
+                       'Should have stored a single meta value with an array containing only the default value.'
+               );
+       }
+
+       /**
+        * Ensures that REST API calls with multi post meta values (containing a list array as
+        * the default) for the registered meta field stores the default value into the database.
+        *
+        * When the default value isn't persisted in the database, a read of the post meta
+        * at some point in the future might return a different value if the code setting the
+        * default changed. This ensures that once a value is intentionally saved into the
+        * database that it will remain durably in future reads.
+        *
+        * Further, the total count of stored values may be wrong if the default value
+        * is culled from the results of a "multi" read.
+        *
+        * @ticket 55600
+        *
+        * @dataProvider data_scalar_default_values
+        *
+        * @param string $type              Scalar type of default value: one of `boolean`, `integer`, `number`, or `string`.
+        * @param mixed  $default_value     Appropriate default value for given type.
+        * @param mixed  $alternative_value Appropriate value for given type that doesn't match the default value.
+        */
+       public function test_array_multi_default_is_saved_to_db( $type, $default_value, $alternative_value ) {
+               $this->grant_write_permission();
+
+               $meta_key_multiple = "with_multi_{$type}_default";
+
+               // Register non-singular post meta for type.
+               register_post_meta(
+                       'post',
+                       $meta_key_multiple,
+                       array(
+                               'type'         => 'array',
+                               'single'       => false,
+                               'show_in_rest' => array(
+                                       'schema' => array(
+                                               'type'  => 'array',
+                                               'items' => array(
+                                                       'type' => $type,
+                                               ),
+                                       ),
+                               ),
+                               'default'      => $default_value,
+                       )
+               );
+
+               $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) );
+               $request->set_body_params(
+                       array(
+                               'meta' => array(
+                                       $meta_key_multiple => array( array( $default_value ) ),
+                               ),
+                       )
+               );
+
+               $response = rest_get_server()->dispatch( $request );
+               $this->assertSame(
+                       200,
+                       $response->get_status(),
+                       "API call should have returned successfully but didn't: check test setup."
+               );
+
+               $this->assertSame(
+                       array( array( $default_value ) ),
+                       get_metadata_raw( 'post', self::$post_id, $meta_key_multiple, false ),
+                       'Should have stored a single meta value with an object representing the default value.'
+               );
+
+               $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) );
+               $request->set_body_params(
+                       array(
+                               'meta' => array(
+                                       $meta_key_multiple => array(
+                                               array( $default_value ),
+                                               array( $alternative_value ),
+                                       ),
+                               ),
+                       )
+               );
+
+               $response = rest_get_server()->dispatch( $request );
+               $this->assertSame(
+                       200,
+                       $response->get_status(),
+                       "API call should have returned successfully but didn't: check test setup."
+               );
+
+               $this->assertSame(
+                       array( array( $default_value ), array( $alternative_value ) ),
+                       get_metadata_raw( 'post', self::$post_id, $meta_key_multiple, false ),
+                       'Should have stored a single meta value with an object representing the default value.'
+               );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * @ticket 48823
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function test_multiple_errors_are_returned_at_once() {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3516,4 +3974,21 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        ),
</span><span class="cx" style="display: block; padding: 0 10px">                );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       /**
+        * Data provider.
+        *
+        * Provides example default values of scalar types;
+        * in contrast to arrays, objects, etc...
+        *
+        * @return array[]
+        */
+       public static function data_scalar_default_values() {
+               return array(
+                       'boolean default' => array( 'boolean', true, false ),
+                       'integer default' => array( 'integer', 42, 43 ),
+                       'number default'  => array( 'number', 42.99, 43.99 ),
+                       'string default'  => array( 'string', 'string', 'string2' ),
+               );
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre>
</div>
</div>

</body>
</html>