Index: src/wp-includes/class-wp-meta-manager.php
===================================================================
--- src/wp-includes/class-wp-meta-manager.php	(nonexistent)
+++ src/wp-includes/class-wp-meta-manager.php	(working copy)
@@ -0,0 +1,193 @@
+<?php
+/**
+ * A registry for meta data.
+ *
+ * @package WordPress
+ * @since 4.6.0
+ */
+final class WP_Meta_Manager {
+	/**
+	 * Registered meta of the post meta type (including pages and CPTs).
+	 *
+	 * @since 4.6.0
+	 * @access private
+	 * @var array
+	 */
+	private $post = array(
+		'all' => array()
+	);
+
+	/**
+	 * Registered meta of the taxonomy meta type.
+	 *
+	 * @since 4.6.0
+	 * @access private
+	 * @var array
+	 */
+	private $taxonomy = array(
+		'all' => array()
+	);
+
+	/**
+	 * Registered meta of the user meta type.
+	 *
+	 * @since 4.6.0
+	 * @access private
+	 * @var array
+	 */
+	private $user = array(
+		'all' => array()
+	);
+
+	/**
+	 * Registered meta of the comment meta type.
+	 *
+	 * @since 4.6.0
+	 * @access private
+	 * @var array
+	 */
+	private $comment = array(
+		'all' => array()
+	);
+
+	/**
+	 * @since 4.6.0
+	 */
+	function __construct() {
+		$post_types_without_meta = array( 'revision', 'nav_menu_item' );
+		foreach ( get_post_types( null, 'names' ) as $post_type ) {
+			if ( in_array( $post_type, $post_types_without_meta ) ) {
+				continue;
+			}
+			$this->post[$post_type] = array();
+		}
+
+		foreach ( get_taxonomies() as $taxonomy ) {
+			$this->taxonomy[$taxonomy] = array();
+		}
+
+		foreach ( array( 'comment', 'ping' ) as $comment_type ) {
+			$this->comment[$comment_type] = array();
+		}
+	}
+
+	/**
+	 * Register a meta key.
+	 *
+	 * @since 4.6.0
+	 *
+	 * @param array $query {
+	 *     Array of meta data properties.
+	 *
+	 *     @type string $object_type  The meta type this key belongs to, e.g. `post`.
+	 *     @type string $object_subtype The meta subtype this key belongs to, e.g. `page`.
+	 *     @type string $key          The meta key.
+	 *     @type array  $schema       The meta field's schema, according to JSON schema.
+	 *     @type array  $public       Whether the meta key should be publicly queryable.
+	 * @return true|WP_Error
+	 */
+	public function register( $args ) {
+		if ( empty( $args['object_type'] ) ) {
+			return new WP_Error( 'missing_object_type', __( 'The object type is required to register meta.' ) );
+		}
+		if ( ! property_exists( $this, $args['object_type'] ) ) {
+			return new WP_Error( 'object_type_does_not_exist', __( sprintf( 'The object type "%s" does not exist.', esc_html( $args['object_type'] ) ) ) );
+		}
+		if ( ! empty( $args['object_subtype'] ) && ! isset( $this->{$args['object_type']}[$args['object_subtype'] ] ) ) {
+			return new WP_Error( 'meta_subtype_does_not_exist', __( 'The meta subtype does not exist in the registry.' ) );
+		}
+
+		if ( empty( $args['key'] ) ) {
+			return new WP_Error( 'meta_key_required', __( 'The meta key is required.' ) );
+		}
+
+		if ( ! isset( $args['schema'] ) ) {
+			return new WP_Error( 'schema_required', __( 'The JSON schema for the meta key is required.' ) );
+		}
+
+		if ( ! isset( $args['public'] ) ) {
+			return new WP_Error( 'public_required', __( 'The public argument is required to register meta.' ) );
+		}
+
+		if ( empty( $args['object_subtype'] ) ) {
+			$args['object_subtype'] = 'all';
+		}
+
+		$this->{$args['object_type']}[$args['object_subtype']][$args['key']] = array(
+			'public' => $args['public'],
+			'schema' => $args['schema'],
+		);
+
+		return true;
+	}
+
+	/**
+	 * Retrieve a list of registered meta keys for an object type and subtype.
+	 *
+	 * @since 4.6.0
+	 *
+	 * @param string $object_type The type of object. Post, comment, user, term.
+	 * @param string $object_subtype A subtype of the object.
+	 *
+	 * @return array List of registered meta keys.
+	 */
+	public function get_registered_meta_keys( $object_type = 'post', $object_subtype = 'all' ) {
+		if ( ! property_exists( $this, $object_type ) ) {
+			return array();
+		}
+
+		if ( ! isset( $this->{$object_type}[ $object_subtype ] ) ) {
+			return array();
+		}
+
+		return $this->{$object_type}[ $object_subtype ];
+	}
+
+	/**
+	 * Check if a meta key is registered.
+	 *
+	 * @since 4.6.0
+	 *
+	 * @param string $object_type
+	 * @param string $meta_key
+	 * @param string $object_subtype
+	 *
+	 * @return bool True if the meta key is registered to the object type and subtype. False if not.
+	 */
+	public function registered_meta_key_exists( $object_type, $meta_key, $object_subtype = 'all' ) {
+		if ( ! property_exists( $this, $object_type ) ) {
+			return false;
+		}
+
+		if ( ! isset( $this->{$object_type}[ $object_subtype ] ) ) {
+			return false;
+		}
+
+		if ( ! isset( $this->{$object_type}[ $object_subtype ][ $meta_key ] ) ) {
+			return false;
+		}
+
+		return true;
+	}
+
+	/**
+	 * Unregister a meta key from the list of registered keys.
+	 *
+	 * @since 4.6.0
+	 *
+	 * @param string $object_type    The type of object.
+	 * @param string $meta_key       The meta key.
+	 * @param string $object_subtype Optional. The subtype of the object type.
+	 *
+	 * @return bool|WP_Error True if successful. WP_Error if the meta key is invalid.
+	 */
+	public function unregister_meta_key( $object_type, $meta_key, $object_subtype = 'all' ) {
+		if ( ! $this->registered_meta_key_exists( $object_type, $meta_key, $object_subtype ) ) {
+			return new WP_Error( 'meta_key_is_not_registered', __( 'Meta key is not registered.' ) );
+		}
+
+		unset( $this->{$object_type}[ $object_subtype ][ $meta_key ] );
+
+		return true;
+	}
+}
Index: src/wp-includes/default-filters.php
===================================================================
--- src/wp-includes/default-filters.php	(revision 37177)
+++ src/wp-includes/default-filters.php	(working copy)
@@ -477,4 +477,7 @@
 add_filter( 'oembed_dataparse',       'wp_filter_oembed_result',        10, 3 );
 add_filter( 'oembed_response_data',   'get_oembed_response_data_rich',  10, 4 );
 
+// Meta
+add_action( 'plugins_loaded', '_wp_initialize_meta_manager', 0 );
+
 unset( $filter, $action );
Index: src/wp-includes/meta.php
===================================================================
--- src/wp-includes/meta.php	(revision 37177)
+++ src/wp-includes/meta.php	(working copy)
@@ -936,14 +936,16 @@
  * Sanitize meta value.
  *
  * @since 3.1.3
+ * @since 4.6.0 Added the `$object_subtype` parameter.
  *
- * @param string $meta_key   Meta key
- * @param mixed  $meta_value Meta value to sanitize
- * @param string $meta_type  Type of meta
+ * @param string $meta_key       Meta key.
+ * @param mixed  $meta_value     Meta value to sanitize.
+ * @param string $object_type    Type of object the meta is registered to.
+ * @param string $object_subtype Optional. Subtype of object. Will inherit the object type by default.
+ *
  * @return mixed Sanitized $meta_value
  */
-function sanitize_meta( $meta_key, $meta_value, $meta_type ) {
-
+function sanitize_meta( $meta_key, $meta_value, $object_type, $object_subtype = 'all' ) {
 	/**
 	 * Filter the sanitization of a specific meta key of a specific meta type.
 	 *
@@ -958,30 +960,220 @@
 	 * @param string $meta_key   Meta key.
 	 * @param string $meta_type  Meta type.
 	 */
-	return apply_filters( "sanitize_{$meta_type}_meta_{$meta_key}", $meta_value, $meta_key, $meta_type );
+	$meta_value = apply_filters( "sanitize_{$object_type}_meta_{$meta_key}", $meta_value, $meta_key, $object_type );
+
+	/**
+	 * Filter the sanitization of a specific meta key of a specific meta object type and subtype.
+	 *
+	 * The dynamic portions of the hook name, `$object_type`, `$object_subtype`, and `$meta_key`
+	 * refer to the metadata object type (comment, post, or user), the subtype of that object type
+	 * and the meta key value respectively.
+	 *
+	 * @since 4.5.0
+	 *
+	 * @param mixed  $meta_value     Meta value to sanitize.
+	 * @param string $meta_key       Meta key.
+	 * @param string $object_type    Meta type.
+	 * @param string $object_subtype Subtype of the object type.
+	 */
+	return apply_filters( "sanitize_{$object_type}_{$object_subtype}_meta_{$meta_key}", $meta_value, $meta_key, $object_type, $object_subtype );
 }
 
 /**
- * Register meta key
+ * Register a meta key.
  *
+ * @todo should we wrap `register_meta()` with `register_meta_key()` to be more explicit?
+ *
  * @since 3.3.0
+ * @since 4.6.0 Modified to support an array of data to attach to registered meta keys. Previous arguments for
+ *              `$sanitize_callback` and `$auth_callback` have been folded into this array.
  *
- * @param string       $meta_type         Type of meta
- * @param string       $meta_key          Meta key
- * @param string|array $sanitize_callback A function or method to call when sanitizing the value of $meta_key.
- * @param string|array $auth_callback     Optional. A function or method to call when performing edit_post_meta, add_post_meta, and delete_post_meta capability checks.
+ * @param string $object_type    Type of object this meta is registered to.
+ * @param string $meta_key       Meta key to register.
+ * @param array  $args {
+ *     Data used to describe the meta key when registered.
+ *
+ *     @type string   $sanitize_callback A function or method to call when sanitizing `$meta_key` data.
+ *     @type string   $auth_callback     Optional. A function or method to call when performing edit_post_meta,
+ *                                       add_post_meta, and delete_post_meta capability checks.
+ *     @type bool     $public            Whether data associated with this meta key can be considered public.
+ *     @type array    $schema            The meta field's schema, according to JSON schema.
+ * }
+ * @return true|WP_Error
  */
-function register_meta( $meta_type, $meta_key, $sanitize_callback, $auth_callback = null ) {
-	if ( is_callable( $sanitize_callback ) )
-		add_filter( "sanitize_{$meta_type}_meta_{$meta_key}", $sanitize_callback, 10, 3 );
+function register_meta( $object_type, $meta_key, $args ) {
+	global $wp_meta_manager;
+	// @todo validate object type as post, comment, user, term?
 
-	if ( empty( $auth_callback ) ) {
-		if ( is_protected_meta( $meta_key, $meta_type ) )
-			$auth_callback = '__return_false';
-		else
-			$auth_callback = '__return_true';
+	// @todo should this be an object similar to post types?
+	$args = array(
+		'sanitize_callback' => null,
+		'old_sanitize_callback' => null,
+		'auth_callback' => null,
+		'old_auth_callback' => null,
+		'object_subtype' => '',
+		'public' => false,
+		'schema' => false,
+	);
+
+	$passed_args = array_slice( func_get_args(), 2 );
+
+	if ( is_callable( $passed_args[0] ) ) {
+		$args['old_sanitize_callback'] = $passed_args[0];
+	} elseif ( isset( $passed_args[0]['sanitize_callback'] ) ) {
+		$args['sanitize_callback'] = $passed_args[0]['sanitize_callback'];
 	}
 
-	if ( is_callable( $auth_callback ) )
-		add_filter( "auth_{$meta_type}_meta_{$meta_key}", $auth_callback, 10, 6 );
+	if ( isset( $passed_args[1] ) && is_callable( $passed_args[1] ) ) {
+		$args['old_auth_callback'] = $passed_args[1];
+	} elseif ( isset( $passed_args[0]['auth_callback'] ) ) {
+		$args['auth_callback'] = $passed_args[0]['auth_callback'];
+	}
+
+	// @todo is there more than one condition here? What if show_in_rest is true, but the
+	// user fails authentication. No schema?
+	if ( isset( $passed_args[0]['show_in_rest'] ) && $passed_args[0]['show_in_rest'] ) {
+		$args['show_in_rest'] = true;
+	}
+
+	if ( isset( $passed_args[0]['type'] ) ) {
+		$args['type'] = $passed_args[0]['type'];
+	}
+
+	if ( isset( $passed_args[0]['description'] ) ) {
+		$args['description'] = $passed_args[0]['description'];
+	}
+
+	// @todo validate object sub types to only those objects that support custom object types?
+	if ( isset( $passed_args[0]['object_subtype'] ) ) {
+		$object_subtype = $passed_args[0]['object_subtype'];
+	} else {
+		$object_subtype = 'all';
+	}
+
+	$error = $wp_meta_manager->register( array(
+		'object_type' => $object_type,
+		'object_subtype' => $object_subtype,
+		'key' => $meta_key,
+		'public' => $args['public'],
+		'schema' => $args['schema'],
+	) );
+
+	if ( is_wp_error( $error ) ) {
+		return $error;
+	}
+
+	// This legacy filter will fire for all subtypes of an object type.
+	if ( is_callable( $args['old_sanitize_callback'] ) ) {
+		add_filter( "sanitize_{$object_type}_meta_{$meta_key}", $args['old_sanitize_callback'], 10, 3 );
+	}
+
+	if ( is_callable( $args['sanitize_callback'] ) ) {
+		add_filter( "sanitize_{$object_type}_{$object_subtype}_meta_{$meta_key}", $args['sanitize_callback'], 10, 4 );
+	}
+
+	// If neither new or legacy `auth_callback` is provided, fallback to `is_protected_meta()`.
+	if ( empty( $args['auth_callback'] ) && empty( $args['old_auth_callback'] ) ) {
+		if ( is_protected_meta( $meta_key, $object_type ) ) {
+			$args['auth_callback'] = '__return_false';
+		} else {
+			$args['auth_callback'] = '__return_true';
+		}
+	}
+
+	// @todo the only core application of this filter is for the `post` object type. What about users, comments, terms?
+	// The auth here is to edit or add meta, not to view.
+	if ( is_callable( $args['old_auth_callback'] ) ) {
+		add_filter( "auth_{$object_type}_meta_{$meta_key}", $args['old_auth_callback'], 10, 6 );
+	}
+
+	// @todo sort above out before going further
+	if ( is_callable( $args['auth_callback'] ) ) {
+		add_filter( "auth_{$object_type}_{$object_subtype}_meta_{$meta_key}", $args['auth_callback'], 10, 7 );
+	}
+
+	return true;
 }
+
+/**
+ * Check if a meta key is registered.
+ *
+ * @since 4.6.0
+ *
+ * @param string $object_type
+ * @param string $meta_key
+ * @param string $object_subtype
+ *
+ * @return bool True if the meta key is registered to the object type and subtype. False if not.
+ */
+function registered_meta_key_exists( $object_type, $meta_key, $object_subtype = 'all' ) {
+	global $wp_meta_manager;
+	return $wp_meta_manager->registered_meta_key_exists( $object_type, $meta_key, $object_subtype );
+}
+
+/**
+ * Unregister a meta key from the list of registered keys.
+ *
+ * @since 4.6.0
+ *
+ * @param string $object_type    The type of object.
+ * @param string $meta_key       The meta key.
+ * @param string $object_subtype Optional. The subtype of the object type.
+ *
+ * @return bool|WP_Error True if successful. WP_Error if the meta key is invalid.
+ */
+function unregister_meta_key( $object_type, $meta_key, $object_subtype = 'all' ) {
+	global $wp_meta_manager;
+	return $wp_meta_manager->unregister_meta_key( $object_type, $meta_key, $object_subtype );
+}
+
+/**
+ * Retrieve registered metadata for a specified object.
+ *
+ * @since 4.6.0
+ *
+ * @param string $object_type    Type of object to request metadata for. (e.g. comment, post, term, user)
+ * @param int    $object_id      ID of the object the metadata is for.
+ * @param string $meta_key       Optional. Registered metadata key. If not specified, retrieve all registered
+ *                               metadata for the specified object.
+ * @param bool   $single         Optional. If true, return only the first value associated
+ *                               with the specified meta key. If false, return an array of values. Default false.
+ * @param string $object_subtype The subtype of the object's type to request metadata for. (e.g. custom post type)
+ *
+ * @return mixed|WP_Error
+ */
+function get_registered_metadata( $object_type = 'post', $object_id, $meta_key = '', $single = false, $object_subtype = 'all' ) {
+	global $wp_meta_manager;
+	if ( ! empty( $meta_key ) && ! registered_meta_key_exists( $object_type, $meta_key, $object_subtype ) ) {
+		return new WP_Error( 'invalid_meta_key', __( 'Invalid meta key. Not registered.' ) );
+	}
+
+	if ( 'post' === $object_type && $object_subtype !== get_post_type( $object_id ) ) {
+		return new WP_Error( 'invalid_meta_key', __( 'Invalid meta key. Not registered for this subtype.' ) );
+	}
+
+	$data = get_metadata( $object_type, $object_id, $meta_key, $single );
+
+	// If a meta key was specified, return the value associated with that key.
+	if ( ! empty( $meta_key ) ) {
+		return $data;
+	}
+
+	$meta_keys = get_registered_meta_keys( $object_type, $object_subtype );
+    $registered_data = array();
+
+	foreach( $meta_keys as $k => $v ) {
+		if ( isset( $data[ $k ] ) ) {
+			$registered_data[ $k ] = $data[ $k ];
+		}
+	}
+
+	return $registered_data;
+}
+
+
+function _wp_initialize_meta_manager() {
+	if ( empty( $GLOBALS['wp_meta_manager'] ) ) {
+		$GLOBALS['wp_meta_manager'] = new WP_Meta_Manager();
+	}
+}
Index: src/wp-settings.php
===================================================================
--- src/wp-settings.php	(revision 37177)
+++ src/wp-settings.php	(working copy)
@@ -133,6 +133,7 @@
 require( ABSPATH . WPINC . '/user.php' );
 require( ABSPATH . WPINC . '/class-wp-user-query.php' );
 require( ABSPATH . WPINC . '/session.php' );
+require( ABSPATH . WPINC . '/class-wp-meta-manager.php' );
 require( ABSPATH . WPINC . '/meta.php' );
 require( ABSPATH . WPINC . '/class-wp-meta-query.php' );
 require( ABSPATH . WPINC . '/class-wp-metadata-lazyloader.php' );
Index: tests/phpunit/tests/meta/registerMeta.php
===================================================================
--- tests/phpunit/tests/meta/registerMeta.php	(nonexistent)
+++ tests/phpunit/tests/meta/registerMeta.php	(working copy)
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * @group meta
+ */
+class Tests_Meta_Register_Meta extends WP_UnitTestCase {
+	protected static $editor_id;
+	protected static $post_id;
+	protected static $comment_id;
+
+	public static function wpSetUpBeforeClass( $factory ) {
+		self::$editor_id = $factory->user->create( array( 'role' => 'editor' ) );
+		self::$post_id = $factory->post->create();
+		self::$comment_id = $factory->comment->create( array( 'comment_post_ID' => self::$post_id ) );
+	}
+
+	public static function wpTearDownAfterClass() {
+		self::delete_user( self::$editor_id );
+		wp_delete_comment( self::$comment_id, true );
+		wp_delete_post( self::$post_id, true );
+	}
+
+	function setUp() {
+		parent::setUp();
+		wp_set_current_user( self::$editor_id );
+	}
+
+	public function _old_sanitize_meta_cb( $meta_value, $meta_key, $meta_type ) {
+		return $meta_key . ' sanitized';
+	}
+
+	public function _new_sanitize_meta_cb( $meta_value, $meta_key, $object_type, $object_subtype ) {
+		return $meta_key . ' sanitized';
+	}
+
+	public function test_register_meta_without_sanitize_callback_registers_meta_key() {
+		register_meta( 'post', 'flight_number', array() );
+		$this->assertTrue( registered_meta_key_exists( 'post', 'flight_number' ) );
+	}
+
+	public function test_register_meta_for_a_category() {
+		register_meta( 'taxonomy', 'category_icon', array( 'object_subtype' => 'category' ) );
+		$this->assertTrue( registered_meta_key_exists( 'taxonomy', 'category_icon', 'category' ) );
+	}
+
+	public function test_register_meta_for_a_comment() {
+		register_meta( 'comment', 'comment_rating', array( 'object_subtype' => 'comment' ) );
+		$this->assertTrue( registered_meta_key_exists( 'comment', 'comment_rating', 'comment' ) );
+	}
+
+	public function test_register_meta_with_sanitize_callback_registers_meta_key() {
+		register_meta( 'post', 'flight_number', array( 'sanitize_callback' => 'esc_html' ) );
+		$this->assertTrue( registered_meta_key_exists( 'post', 'flight_number' ) );
+	}
+
+	public function test_register_meta_valid_object_type_with_valid_subtype() {
+		register_meta( 'post', 'longitude', array( 'object_subtype' => 'post' ) );
+		$this->assertTrue( registered_meta_key_exists( 'post', 'longitude', 'post' ) );
+	}
+
+	public function test_register_meta_valid_object_type_with_invalid_subtype() {
+		register_meta( 'post', 'latitude', array( 'object_subtype' => 'invalid-type' ) );
+		$this->assertFalse( registered_meta_key_exists( 'post', 'latitude', 'invalid-type' ) );
+	}
+
+	public function test_register_meta_with_old_sanitize_callback_parameter() {
+		register_meta( 'post', 'old_sanitized_key', array( $this, '_old_sanitize_meta_cb' ) );
+		$meta = sanitize_meta( 'old_sanitized_key', 'unsanitized', 'post' );
+
+		$this->assertEquals( 'old_sanitized_key sanitized', $meta );
+	}
+
+	public function test_register_meta_with_new_sanitize_callback_parameter() {
+		register_meta( 'post', 'new_sanitized_key', array( 'sanitize_callback' => array( $this, '_new_sanitize_meta_cb' ) ) );
+		$meta = sanitize_meta( 'new_sanitized_key', 'unsanitized', 'post' );
+
+		$this->assertEquals( 'new_sanitized_key sanitized', $meta );
+	}
+}
