Skip to content

Latest commit

 

History

History
898 lines (630 loc) · 35.7 KB

README.adoc

File metadata and controls

898 lines (630 loc) · 35.7 KB

3D Metadata Specification

Overview

The 3D Metadata Specification defines a standard format for structured metadata in 3D content. Metadata — represented as entities and properties — may be closely associated with parts of 3D content, with data representations appropriate for large, distributed datasets. For the most detailed use cases, properties allow vertex- and texel-level associations; higher-level property associations are also supported.

Many domains benefit from structured metadata — typical examples include historical details of buildings in a city, names of components in a CAD model, descriptions of regions on textured surfaces, and classification codes for point clouds.

The specification defines core concepts to be used by multiple 3D formats, and is language and format agnostic. This document defines concepts with purpose and terminology, but does not impose a particular schema or serialization format for implementation. For use of the format outside of abstract conceptual definitions, see:

  • 3D Tiles Metadata - Assigns metadata to tilesets, tiles, and contents in 3D Tiles 1.1

  • 3DTILES_metadata - An extension for 3D Tiles 1.0 that assigns metadata to tilesets, tiles, and contents

  • EXT_structural_metadata (glTF 2.0) —  Assigns metadata to vertices, texels, and features in a glTF asset

The specification does not enumerate or define the semantic meanings of metadata, and assumes that separate specifications will define semantics for their particular application or domain. One example is the 3D Metadata Semantic Reference which defines built-in semantics for 3D Tiles and glTF. Identifiers for externally-defined semantics can be stored within the 3D Metadata Specification.

Concepts

This specification defines metadata schemas and methods for encoding metadata.

Schemas contain a set of classes and enums. A class represents a category of similar entities, and is defined as a set of properties. Each property describes values of a particular type. An enum defines a set of named values representing a single value type, and may be referenced by class properties. Schema definitions do not describe how entities or properties are stored, and may be represented in a file format in various ways. Schemas can be reused across multiple assets or even file formats.

Entities are instantiations of a class, populated with property values conforming to the class definition. Every property value of an entity shall be defined by its class, and an entity shall not have extraneous property values. Properties of a class may be required, in which case all entities instantiating the class are required to include them.

Note
Informative

Entities may be defined at various levels of abstraction. Within a large dataset, individual vertices or texels may represent entities with granular metadata properties. Vertices and texels may be organized into higher-order groups (such as meshes, scene graphs, or tilesets) having their own associated properties.

Metadata, as used throughout this specification, refers to any association of 3D content with entities and properties, such that entities represent meaningful units within an overall structure. Other common definitions of metadata, particularly in relation to filesystems or networking as opposed to 3D content, remain outside the scope of the document.

Property values are stored with flexible representations to allow compact transmission and efficient lookups. This specification defines two possible storage formats.

Identifiers

Throughout this specification, IDs (identifiers) are strings that match the regular expression ^[a-zA-Z_][a-zA-Z0-9_]*$: Strings that consist of upper- or lowercase letters, digits, or underscores, starting with either a letter or an underscore. These strings should be camel case strings that are human-readable (wherever possible). When IDs subject to these restrictions are not sufficiently clear for human readers, applications should also provide a name for the structures that support dedicated names.

Schemas

Schema

A schema defines the organization and types of metadata used in 3D content, represented as a set of classes and enums. Class definitions are referenced by entities whose metadata conforms to the class definition. This provides a consistent and machine-readable structure for all entities in a dataset.

Components of a schema are listed below, and implementations may define additional components.

ID

IDs (id) are unique identifiers for a schema.

Note
Informative

The schema ID is required for each schema. The main purpose of this ID is to be able to resolve possible ambiguities. The exact mechanism for this disabiguation depends on the client application, and how the access to metadata properties is implemented. But the schema ID makes sure that a compound identifier of the form <schema-id>.<class-name>.<property-name> is globally unique, even when the same class name and property name appears in multiple schemas.

Version

Schema version (version) is an application-specific identifier for a given schema revision. Version shall be a string, and should be syntactically compatible with SemVer.

When a schema has multiple versions, the (id, version) pair uniquely identifies a particular schema and revision.

Note
Example

Valid semantic versions include strings like 0.1.2, 1.2.3, and 1.2.3-alpha.

Name

Names (name) provide a human-readable label for a schema, and are not required to be unique. Names shall be valid Unicode strings, and should be written in natural language.

Description

Descriptions (description) provide a human-readable explanation of a schema, its purpose, or its contents. Typically at least a phrase, and possibly several sentences or paragraphs. Descriptions shall be valid Unicode strings.

Enums

Unordered set of enums.

Classes

Unordered set of classes.


Enum

An enum consists of a set of named values, represented as (string, integer) pairs. Each enum is identified by a unique ID.

Note
Example

A "species" enum with three possible tree species, as well as an "Unknown" value.

  • ID: "species"

  • Name: "Species"

  • Description: "Common tree species identified in the study."

  • Value type: INT32

Table 1. Names and values of an example enum
name value

"Oak"

0

"Pine"

1

"Maple"

2

"Unknown"

-1

ID

IDs (id) are unique identifiers for an enum within a schema.

Name

Names (name) provide a human-readable label for an enum, and are not required to be unique within a schema. Names shall be valid Unicode strings, and should be written in natural language.

Description

Descriptions (description) provide a human-readable explanation of an enum, its purpose, or its contents. Typically at least a phrase, and possibly several sentences or paragraphs. Descriptions shall be valid Unicode strings.

Values

An enum consists of a set of named values, represented as (string, integer) pairs. The following enum value types are supported: INT8, UINT8, INT16, UINT16, INT32, UINT32, INT64, and UINT64. See the Component Type section for definitions of each. Smaller enum types limit the range of possible enum values, and allow more efficient binary encoding. Duplicate names or values within the same enum are not allowed.


Class

Classes represent categories of similar entities, and are defined by a collection of one or more properties shared by the entities of a class. Each class has a unique ID within the schema, and each property has a unique ID within the class, to be used for references within the schema and externally.

ID

IDs (id) are unique identifiers for a class within a schema.

Name

Names (name) provide a human-readable label for a class, and are not required to be unique within a schema. Names shall be valid Unicode strings, and should be written in natural language.

Description

Descriptions (description) provide a human-readable explanation of a class, its purpose, or its contents. Typically at least a phrase, and possibly several sentences or paragraphs. Descriptions shall be valid Unicode strings.

Properties

Unordered set of properties.


Property

Overview

Properties describe the type and structure of values that may be associated with entities of a class. Entities may omit values for a property, unless the property is required. Entities shall not contain values other than those defined by the properties of their class.

Note
Example

The following example shows the basics of how classes describe the types of metadata. A building class describes the heights of various buildings in a dataset. Likewise, the tree class describes trees that have a height, species, and leaf color.

building

Table 2. Properties and types of a building
property type componentType

height

SCALAR

FLOAT32

tree

Table 3. Properties and types of a tree
property type componentType enumType

height

SCALAR

FLOAT32

species

ENUM

species

leafColor

STRING

ID

IDs (id) are unique identifiers for a property within a class.

Name

Names (name) provide a human-readable label for a property, and shall be unique to a property within a class. Names shall be valid Unicode strings, and should be written in natural language. Property names do not have inherent meaning; to provide such a meaning, a property shall also define a semantic.

Note
Example

A typical ID / Name pair, in English, would be localTemperature and "Local Temperature". In Japanese, the name might be represented as "きおん". Because IDs are restricted to identifiers, use of helpful property names is essential for clarity in many languages.

Description

Descriptions (description) provide a human-readable explanation of a property, its purpose, or its contents. Typically at least a phrase, and possibly several sentences or paragraphs. Descriptions shall be valid Unicode strings. To provide a machine-readable semantic meaning, a property shall also define a semantic.

Semantic

Property IDs, names, and descriptions do not have an inherent meaning. To provide a machine-readable meaning, properties may be assigned a semantic identifier string (semantic), indicating how the property’s content should be interpreted. Semantic identifiers may be defined by the 3D Metadata Semantic Reference or by external semantic references, and may be application-specific. Identifiers should be uppercase, with underscores as word separators.

Note
Example

Semantic definitions might include temperature in degrees Celsius (e.g. TEMPERATURE_DEGREES_CELSIUS), time in milliseconds (e.g. TIME_MILLISECONDS), or mean squared error (e.g. MEAN_SQUARED_ERROR). These examples are only illustrative.

Type

A property’s type (type) describes the structure of the value given for each entity.

Table 4. Types of metadata properties
name type

SCALAR

Single numeric component

VEC2

Fixed-length vector with two (2) numeric components

VEC3

Fixed-length vector with three (3) numeric components

VEC4

Fixed-length vector with four (4) numeric components

MAT2

2x2 matrix with numeric components

MAT3

3x3 matrix with numeric components

MAT4

4x4 matrix with numeric components

STRING

A sequence of characters

BOOLEAN

True or false

ENUM

An enumerated type

Component Type

Scalar, vector, and matrix types comprise of numeric components. Each component is an instance of the property’s component type (componentType), with the following component types supported:

Table 5. Component types of metadata properties
name componentType

INT8

Signed integer in the range [-128, 127]

UINT8

Unsigned integer in the range [0, 255]

INT16

Signed integer in the range [-32768, 32767]

UINT16

Unsigned integer in the range [0, 65535]

INT32

Signed integer in the range [-2147483648, 2147483647]

UINT32

Unsigned integer in the range [0, 4294967295]

INT64

Signed integer in the range [-9223372036854775808, 9223372036854775807]

UINT64

Unsigned integer in the range [0, 18446744073709551615]

FLOAT32

A number that can be represented as a 32-bit IEEE floating point number

FLOAT64

A number that can be represented as a 64-bit IEEE floating point number

Floating-point properties (FLOAT32 and FLOAT64) shall not include values NaN, +Infinity, or -Infinity.

Note
Informative

Developers of authoring tools should be aware that many JSON implementations support only numeric values that can be represented as IEEE-754 double precision floating point numbers. Floating point numbers should be representable as double precision IEEE-754 floats when encoded in JSON. When those numbers represent property values (such as noData, min, or max) having lower precision (e.g. single-precision float, 8-bit integer, or 16-bit integer), the values should be rounded to the same precision in JSON to avoid any potential mismatches. Numeric property values encoded in binary storage are unaffected by these limitations of JSON implementations.

Enum Type

Enum properties are denoted by ENUM. An enum property shall additionally provide the ID of the specific enum it uses, referred to as its enum type (enumType).

Arrays

A property can be declared to be a fixed- and variable-length array, consisting of elements of the given type. For fixed-length arrays, a count (count) denotes the number of elements in each array, and shall be greater than or equal to 2. Variable-length arrays do not define a count and may have any length, including zero.

Normalized Values

Normalized properties (normalized) provide a compact alternative to larger floating-point types. Normalized values are stored as integers, but when accessed are transformed to floating-point according to the following equations:

Table 6. Conversion of component types
componentType int to float float to int

INT8

f = max(i / 127.0, -1.0)

i = round(f * 127.0)

UINT8

f = i / 255.0

i = round(f * 255.0)

INT16

f = max(i / 32767.0, -1.0)

i = round(f * 32767.0)

UINT16

f = i / 65535.0

i = round(f * 65535.0)

INT32

f = max(i / 2147483647.0, -1.0)

i = round(f * 2147483647.0)

UINT32

f = i / 4294967295.0

i = round(f * 4294967295.0)

INT64

f = max(i / 9223372036854775807.0, -1.0)

i = round(f * 9223372036854775807.0)

UINT64

f = i / 18446744073709551615.0

i = round(f * 18446744073709551615.0)

normalized is only applicable to scalar, vector, and matrix types with integer component types.

Note
Informative

Depending on the implementation and the chosen integer type, there may be some loss of precision in values after denormalization. For example, if the implementation uses 32-bit floating point variables to represent the value of a normalized 32-bit integer, there are only 23 bits in the mantissa of the float, and lower bits will be truncated by denormalization. Client implementations should use higher precision floats when appropriate for correctly representing the result.

Offset and Scale

A property may declare an offset (offset) and scale (scale) to apply to property values. This is useful when mapping property values to a different range.

The offset and scale can be defined for types that either have a floating-point componentType, or when normalized is set to true. This applies to SCALAR, VECN, and MATN types, and to fixed-length arrays of these types. The structure of offset and scale is explained in the Property Values Structure section.

The following equation is used to transform the original property value into the actual value that is used by the client:

transformedValue = offset + scale * normalize(value)

These operations are applied component-wise, both for array elements and for vector and matrix components.

The transformation that is described here allows arbitrary source value ranges to be mapped to arbitrary target value ranges, by first computing the float value for the original normalized value, and then mapping that floating point range to the desired target range.

Note
Informative

The result of transforming a normalized integer value into a floating point value may be lossy, as described in the section about Normalized Values. Depending on the range of property values, the values of offset and scale, and the floating point precision that is used in the client implementation, the computation may cause low-significance bits to be truncated from the final result. Client implementations should retain as much precision as reasonably possible.

When the offset for a property is not given, then is is assumed to be 0 for each component of the respective type. When the scale value of a property is not given, then it is assumed to be 1 for each component of the respective type. Instances of the class that defines the respective property can override the offset- and scale factors, to account for the actual range of property values that are provided by the instance.

Minimum and Maximum Values

Properties may specify a minimum (min) and maximum (max) value. Minimum and maximum values represent component-wise bounds of the valid range of values for a property. Both values are inclusive, meaning that they denote the smallest and largest allowed value, respectively.

The min and max value can be defined for SCALAR, VECN, and MATN types with numeric component types, and for fixed-length arrays of these types. The structure of min and max is explained in the Property Values Structure section.

For properties that are normalized, the component type of min and max is a floating point type. Their values represent the bounds of the final, transformed property values. This includes the normalization and offset- or scale computations, as well as other transforms or constraints that are not part of the class definition itself: A normalized unsigned value is in the range [0.0, 1.0] after the normalization has been applied, but [min, max] may specify a different value range.

For all other properties, the component type of min and max matches the componentType of the property, and the values are the bounds of the original property values.

Note
Example

A property storing GPS coordinates might define a range of [-180, 180] degrees for longitude values and [-90, 90] degrees for latitude values.

Property values outside the [minimum, maximum] range are not allowed, with the exception of noData values.

Required Properties, No Data Values, and Default Values

When associated property values shall exist for all entities of a class, a property is considered required (required).

Individual elements in an array or individual components in a vector or matrix cannot be marked as required; only the property itself can be marked as required.

Properties may optionally specify a No Data value (noData, or "sentinel value") to be used when property values do not exist. A noData value may be provided for any type except BOOLEAN. For ENUM types, a noData value should contain the name of the enum value as a string, rather than its integer value. The structure of the noData value is explained in the Property Values Structure section.

A noData value is especially useful when only some entities in a property table are missing property values (see Binary Table Format). Otherwise if all entities are missing property values the column may be omitted from the table and a noData value need not be provided. Entities encoded in the JSON Format may omit the property instead of providing a noData value. noData values and omitted properties are functionally equivalent.

A default value (default) may be provided for missing property values. For ENUM types, a default value should contain the name of the enum value as a string, rather than its integer value. For all other cases, the structure of the default value is explained in the Property Values Structure section.

If a default value is not provided, the behavior when encountering missing property values is implementation-defined.

Note
Example

In the example below, a "tree" class is defined with noData indicating a specific enum value to be interpreted as missing data.

Table 7. Properties and types of a tree
property componentType required noData

height

FLOAT32

Yes

species

ENUM

"Unknown"

leafColor

STRING

Yes

Property Values Structure

Property values that appear as part of the class definition are the offset, scale, minimum, maximum, default values and no-data values. The structure of these values inside the class definition depends on the type of the property. For SCALAR (non-array) types, they are single values. For all other cases, they are arrays:

  • For SCALAR array types with fixed length count, they are arrays with length count.

  • For VECN types, they are arrays, with length N.

  • For MATN types, they are arrays, with length N * N.

  • For VECN array types with fixed length count, they are arrays with length count, where each array element is itself an array of length N

  • For MATN array types with fixed length count, they are arrays with length count, where each array element is itself an array of length N * N.

For noData values and numeric values that are not normalized, the type of the innermost elements of these arrays corresponds to the componentType. For numeric values that are normalized, the innermost elements are floating-point values.

Storage Formats

Overview

Schemas provide templates for entities, but creating an entity requires specific property values and storage. This section covers two storage formats for entity metadata:

  • Binary Table Format - property values are stored in parallel 1D arrays, encoded as binary data

  • JSON Format - property values are stored in key/value dictionaries, encoded as JSON objects

Both formats are suitable for general purpose metadata storage. Binary formats may be preferable for larger quantities of metadata.

Additional serialization methods may be defined outside of this specification. For example, property values could be stored in texture channels or retrieved from a REST API as XML data.

Note
Informative

Any specification that references 3D Metadata shall state explicitly which storage formats are supported, or define its own serialization. For example, the EXT_structural_metadata glTF extension implements the binary table format described below, and defines an additional image-based format for per-texel metadata.

Binary Table Format

Overview

The binary table format is similar to a database table where entities are rows and properties are columns. Each column represents one of the properties of the class. Each row represents a single entity conforming to the class.

Table Format
Figure 1. Illustration of metadata that can be stored in a table

The rows of a table are addressed by an integer index called an entity ID. Entity IDs are always numbered 0, 1, ..., N - 1 where N is the number of rows in the table.

Property values are stored in parallel arrays called property arrays, one per column. Each property array stores values for a single property. The i-th value of each property array is the value of that property for the entity with an entity ID of i.

Binary encoding is efficient for runtime use, and scalable to large quantities of metadata. Because property arrays contain elements of a single type, bitstreams may be tightly packed or may use compression methods appropriate for a particular data type.

Property values are binary-encoded according to their data type, in little-endian format. Values are tightly packed: there is no padding between values.

Scalars

A scalar value is encoded based on the componentType. Multiple values are packed tightly in the same buffer. The following data types are supported:

Table 8. Types for scalar metadata values
Name Description

INT8

8-bit two’s complement signed integer

UINT8

8-bit unsigned integer

INT16

16-bit two’s complement signed integer

UINT16

16-bit unsigned integer

INT32

32-bit two’s complement signed integer

UINT32

32-bit unsigned integer

INT64

64-bit two’s complement signed integer

UINT64

64-bit unsigned integer

FLOAT32

32-bit IEEE floating point number

FLOAT64

64-bit IEEE floating point number

Vectors

Vector components are tightly packed and encoded based on the componentType.

Matrices

Matrix components are tightly packed in column-major order and encoded based on the componentType.

Booleans

A boolean value is encoded as a single bit, either 0 (false) or 1 (true). Multiple boolean values are packed tightly in the same buffer. These buffers of tightly-packed bits are sometimes referred to as bitstreams.

For a table with N rows, the buffer that stores these boolean values will consist of ceil(N / 8) bytes. When N is not divisible by 8, then the unused bits of the last byte of this buffer shall be set to 0.

Note
Informative

Example accessing a boolean value for entity ID i.

byteIndex = floor(i / 8)
bitIndex = i % 8
bitValue = (buffer[byteIndex] >> bitIndex) & 1
value = bitValue == 1

Strings

A string value is a UTF-8 encoded byte sequence. Multiple strings are packed tightly in the same buffer.

Because string lengths may vary, a string offset buffer is used to identify strings in the buffer. If there are N strings in the property array, the string offset buffer has N + 1 elements. The first N of these point to the first byte of each string, while the last points to the byte immediately after the last string. The number of bytes in the i-th string is given by stringOffset[i + 1] - stringOffset[i]. UTF-8 encodes each character as 1-4 bytes, so string offsets do not necessarily represent the number of characters in the string.

The data type used for offsets is defined by a string offset type, which may be UINT8, UINT16, UINT32, or UINT64.

Note
Example

Three UTF-8 strings, binary-encoded in a buffer.

String property example
Figure 2. Data layout for the buffers storing string metadata

Enums

Enums are encoded as integer values according to the enum value type (see Enums). Any integer data type supported for Scalars may be used for enum values.

Fixed-Length Arrays

A fixed-length array value is encoded as a tightly packed array of count elements, where each element is encoded according to the type.

Variable-Length Arrays

Variable-length arrays use an additional array offset buffer. The i-th value in the array offset buffer is an element index — not a byte offset — identifying the beginning of the i-th array. String values within an array may have inconsistent lengths, requiring both array offset and string offset buffers (see: Strings).

The data type used for offsets is defined by an array offset type, which may be UINT8, UINT16, UINT32, or UINT64.

If there are N arrays in the property array, the array offset buffer has N + 1 elements. The first N of these point to the first element of an array within the property array, or within a string offset buffer for string component types. The last value points to a (non-existent) element immediately following the last array element.

For each case below, the offset of an array element i within its binary storage is expressed in terms of entity ID id and element index i.

Table 9. Offset types for strings and arrays
Type Offset type Offset

STRING

byte offset

stringOffset[arrayOffset[id] + i]

All other types

array index

arrayOffset[id] + i

Each expression in the table above defines an index into the underlying property array. For a property array of SCALAR elements with FLOAT32 component type, index 3 corresponds to byte offset 3 * sizeof(FLOAT32). For a property array of VEC4 elements with FLOAT32 component type, index 3 corresponds to byte offset 3 * 4 * sizeof(FLOAT32) = 48. For an array of BOOLEAN elements, offset 3 would correspond to bit offset 3.

Note
Example

Five variable-length arrays of UINT8 components, binary-encoded in a buffer. The associated property definition would be type = "SCALAR", componentType = "UINT8", and array = true.

Variable-length array
Figure 3. Data layout for the buffers storing string variable-length arrays
Note
Example

Two variable-length arrays of strings, binary-encoded in a buffer. The associated property definition would be type = "STRING" and array = true (variable-length). Observe that the last element of the array offset buffer points to the last element of the string offset buffer. This is because the last valid string offset is the next-to-last element of the string offset buffer.

Variable-length array of string
Figure 4. Data layout for the buffers storing string variable-length arrays of strings

JSON Format

Overview

JSON encoding is useful for storing a small number of entities in human readable form.

Each entity is represented as a JSON object with its class identified by a string ID. Property values are defined in a key/value properties dictionary, having property IDs as its keys. Property values are encoded as corresponding JSON types: numeric types are represented as number, booleans as boolean, strings as string, enums as string, vectors and matrices as array of number, and arrays as array of the containing type.

Note
Example

The following example demonstrates usage for both fixed- and variable-length arrays:

An enum, "basicEnum", composed of three (name: value) pairs:

Table 10. Names and values of an example enum
name value

"Enum A"

0

"Enum B"

1

"Enum C"

2

A class, "basicClass", composed of ten properties. stringArrayProperty count is undefined and therefore variable-length.

Table 11. Properties of an example class
id type componentType array count enumType required

floatProperty

SCALAR

FLOAT64

false

Yes

integerProperty

SCALAR

INT32

false

Yes

vectorProperty

VEC2

FLOAT32

false

Yes

floatArrayProperty

SCALAR

FLOAT32

true

3

Yes

vectorArrayProperty

VEC2

FLOAT32

true

2

Yes

booleanProperty

BOOLEAN

false

Yes

stringProperty

STRING

false

Yes

enumProperty

ENUM

false

basicEnum

Yes

stringArrayProperty

STRING

true

Yes

optionalProperty

STRING

false

A single entity encoded in JSON. Note that the optional property is omitted in this example.

{
  "entity": {
    "class": "basicClass",
    "properties": {
      "floatProperty": 1.5,
      "integerProperty": -90,
      "vectorProperty": [0.0, 1.0],
      "floatArrayProperty": [1.0, 0.5, -0.5],
      "vectorArrayProperty": [[0.0, 1.0], [1.0, 2.0]],
      "booleanProperty": true,
      "stringProperty": "x123",
      "enumProperty": "Enum B",
      "stringArrayProperty": ["abc", "12345", "おはようございます"]
    }
  }
}

Scalars

All component types (INT8, UINT8, INT16, UINT16, INT32, UINT32, INT64, UINT64, FLOAT32, and FLOAT64) are encoded as JSON numbers. Floating point values shall be representable as IEEE floating point numbers.

Note
Informative

For numeric types the size in bits is made explicit. Even though JSON only has a single number type for all integers and floating point numbers, the application that consumes the JSON may make a distinction. For example, C and C++ have several different integer types such as uint8_t, uint32_t. The application is responsible for interpreting the metadata using the type specified in the property definition.

Vectors

Vectors are encoded as a JSON array of numbers.

Matrices

Matrices are encoded as a JSON array of numbers in column-major order.

Booleans

Booleans are encoded as a JSON boolean, either true or false.

Strings

Strings are encoded as JSON strings.

Enums

Enums are encoded as JSON strings using the name of the enum value rather than the integer value. Therefore the enum value type, if specified, is ignored for the JSON encoding.

Arrays

Arrays are encoded as JSON arrays, where each element is encoded according to the type. When a count is specified, the length of the JSON array shall match the count. Otherwise, for variable-length arrays, the JSON array may be any length, including zero-length.