Ion Schema Specification 1.0

This specification defines a means to express constraints over the Ion data model. The universe of values in the Ion data model is narrowed by defining types with constraints, then determining whether a value is valid for a particular type. Types are expressed with the Ion Schema Language (ISL), which is comprised of the syntax, constraints, and grammar presented in this document. Finally, a set of examples are provided to illustrate how the various aspects of ISL work together. This document assumes that readers are familiar with the Ion data model defined in the Amazon Ion Specification.

Type System

The type system is divided into the following two categories:

Core Types

The type system defines the following core types:

If not specified, the default type is any.

The core types do not include any of Ion’s null.* values, but each of the types may have a weakly- or strongly-typed null value if the type name is annotated with nullable. When a strongly-typed null value is encountered, its type must agree with one of the core types of the expected type. For example, if a nullable::any_of[int, string, struct] is expected, 5, "hi", {}, null, null.null,, null.string, and null.struct are all valid values, but null.decimal is not.

Ion Types

The Ion types are prefixed with $, and correspond precisely with the types defined by the Ion data model, including strongly-typed null values:

Schema Definitions

A schema consists of a schema version marker $ion_schema_1_0 followed by an optional schema header, zero or more type definitions, and an optional schema footer. The schema header is a struct with an optional imports field for leveraging types from other schemas. While a header and footer are both optional, a footer is required if a header is present (and vice-versa). A schema is defined with an Ion document of the following form:


schema_header::{         // optional
  imports: [
    { id: "com/example/Insects.isl" },
    { id: "arn:aws::::com/example/autos", type: Truck },
    { id: "", type: Feline, as: Cat },


schema_footer::{         // optional


An import allows types from other schemas to be used within a schema definition. An import that only specifies an id makes all of the types from that schema available for use in the current schema. Specifying a type narrows the import to that single type, and a type may be imported with a different name by specifying: as: <TYPE_ALIAS>. The core types and Ion types are implicitly imported before any specified imports; specified imports are performed in order, and an import that cannot be resolved must result in an error. If two types with the same name are imported, or if a type defined within a schema has the same name as an imported type, this must result in an error.

Schema Authorities

The structure of a id string (per the example above) is defined by the schema authority responsible for the schema/type(s) being imported. Note that runtime resolution of a schema over a network presents availability and security risks, and should thereby be avoided.

When resolving a schema, authorities may choose to follow well-known patterns; for example:

Type Definitions

A type consists of a collection of zero or more constraints and an optional name. Unless otherwise specified, type definitions have an implicit constraint type: any, and thereby represent any non-null value from the universe of values representable in the Ion data model. In order for a value to be a valid instance of a type, the value must not violate any of the type’s constraints.

Types are defined with Ion of the following form:

  name: <TYPE_NAME>,

When referring to a type, it may be identified by name or alias (if it was imported with an alias), or a fully-qualified import-style reference ({ id: "...", type: ... }). Additionally, an unnamed type may be inlined anywhere a <TYPE_REFERENCE> is expected; in such cases, the type annotation is optional. For example, a list containing strings of exactly 10 codepoints may be defined with an inline type as follows:

  type: list,              // type reference
  element: {               // inline type
    type: string,
    codepoint_length: 10,

Open Content

The default behavior for containers is to allow additional content beyond what is explicitly specified for a given type; this is referred to as open content. For a given type that is not constrained by content: closed, the following open content is considered valid as long as the content doesn’t exceed any specified constraints:

Since annotations are considered to be metadata of a value, specifying additional annotations on a value is valid independent of whether a type is constrained by content: closed.

ISL itself allows for open content – additional information may be specified within a type definition (or schema_header / schema_footer), and such additional content is simply ignored.


Constraints narrow the universe of values from the Ion data model. Constraints below are grouped by the type of data for which they are applicable. Note that constraints may conflict with each other. For example, there is no value that can satisfy the following constraints:

  type: int,
  codepoint_length: 5,

Null Values

Generally speaking, constraints must reject null values as invalid. For example, the precision and scale constraints must reject a null value, as null doesn’t have a precision or scale to evaluate; the fields constraint must reject null.struct, as null.struct doesn’t have any fields. Similar reasoning applies to the expected handling of null values by most constraints. The contains, type, and valid_values constraints are exceptions to this, as these constraints may be defined such that a null value is valid.


range::[ <RANGE_TYPE>, <RANGE_TYPE> ]
range::[ min, <RANGE_TYPE> ]
range::[ <RANGE_TYPE>, max ]
range::[ min, max ]

Some constraints can be defined by a range. A range is represented by a list annotated with range, and containing two values, in order: the minimum and maximum ends of the range. The default behavior is for both ends of the range to be inclusive; if exclusive behavior is desired, the minimum or maximum (or both) values shall be annotated with exclusive. If the minimum or maximum end of a range is to be unspecified, this shall be represented by the symbols min or max, respectively; the exclusive annotation is not applicable when the symbols min or max are specified.

range::[5, max]                        // minimum 5, maximum unbound
range::[min, 7]                        // minimum unbound, maximum 7
range::[5, 7]                          // between 5 and 7, inclusive
range::[exclusive::5, exclusive::7]    // only 6 is valid
range::[5.5, 7.9]                      // between 5.5 and 7.9, inclusive

General Constraints


annotations: [ <ANNOTATION>... ]
annotations: required::[ <ANNOTATION>... ]
annotations: ordered::[ <ANNOTATION>... ]

Indicates the annotations that may be specified on values of the type. By default, individual annotations are optional; this default may be overridden by annotating the annotations list with required. Additionally, each annotation may be annotated with optional or required to override the list-level behavior. If annotations must be applied to values in the specified order, the list of annotations may be annotated with ordered. If there are multiple annotations on the annotations list, they may be specified in any order.

Note that annotations represent metadata for a value, and additional annotations on a value are valid independent of whether a type is constrained by content: closed.

annotations: [red, required::green, blue]
annotations: required::[red, optional::green, blue]
annotations: required::ordered::[one, optional::two, three]


type: nullable::<TYPE_REFERENCE>

Indicates the type that a value shall be validated against. The core types do not include null (weak- or strong-typed); for cases in which null is a desired value, annotate the <TYPE_REFERENCE> with nullable. When a strongly-typed null value is encountered, its type must agree with the expected type (e.g., if a nullable::int is expected, 5, null, null.null, and are valid, but null.string is not).

{ type: int }
{ type: nullable::int }


valid_values: [ <VALUE>... ]
valid_values: <RANGE<NUMBER>>

A list of acceptable, non-annotated values; any values not present in the list are invalid. Whether a particular value matches a specified valid_value is governed by the equivalence rules defined by the Ion data model (e.g., 1.230 is not valid for valid_values: [1.23], as it has a different precision). For numeric and timestamp types, valid_values may optionally be defined as a range. When a timestamp range is specified, neither end of the range may have an unknown local offset.

valid_values: [2, 3, 5, 7, 11, 13, 17, 19]
valid_values: ["abc", "def", "ghi"]
valid_values: [[1], [2.0, 3.0], [three, four, five]]
valid_values: [2000T, 2004T, 2008T, 2012T]
valid_values: range::[-100, max]
valid_values: range::[min, 100]
valid_values: range::[-100, 100]
valid_values: range::[0, 100.0]
valid_values: range::[exclusive::0d0, exclusive::1]
valid_values: range::[-0.12e4, 0.123]
valid_values: range::[2000-01-01T00:00:00Z, max]
valid_values: [1, 2, 3, null,]

Blob/Clob Constraints


byte_length: <INT>
byte_length: <RANGE<INT>>

The exact or minimum/maximum number of bytes in a blob or clob (note that this constrains the number of bytes in the input source, which may differ from the number of bytes needed to serialize the blob/clob).

byte_length: 5
byte_length: range::[10, max]
byte_length: range::[min, 100]
byte_length: range::[10, 100]

String/Symbol Constraints


codepoint_length: <INT>
codepoint_length: <RANGE<INT>>

The exact or minimum/maximum number of Unicode codepoints in a string or symbol. Note that characters are a complex topic in Unicode, whereas codepoints provide an unambiguous unit for constraining the length of a string or symbol.

codepoint_length: 5
codepoint_length: range::[10, max]
codepoint_length: range::[min, 100]
codepoint_length: range::[10, 100]


regex: <STRING>
regex: i::<STRING>
regex: m::<STRING>
regex: i::m::<STRING>

A string that conforms to a RegularExpressionBody defined by ECMA 262 Regular Expressions. Regular expressions shall be limited to the following features:

  Unicode codepoints match themselves
[abc] codepoint class
[a-z] range codepoint class
[^abc] complemented codepoint class
[^a-z] complemented range codepoint class
^ anchor at the beginning of the input
$ anchor at the end of the input
(...) grouping
| alternation
? zero or one
* zero or more
+ one or more
?? zero or one (lazy)
*? zero or more (lazy)
+? one or more (lazy)
{x} exactly x occurrences
{x,} at least x occurrences
{x,y} at least x and at most y occurrences
{x}? exactly x occurrences (lazy)
{x,}? at least x occurrences (lazy)
{x,y}? at least x and at most y occurrences (lazy)

Regular expression flags may be specified as annotations on the regular expression string; supported flags shall include:

i case insensitive
m ^ and $ match at line breaks
regex: "M(iss){2}ippi"
regex: i::"susie"
regex: i::m::"^B[0-9]{9}$"

Decimal Constraints


precision: <INT>
precision: <RANGE<INT>>

An exact or minimum/maximum indicating the number of digits in the unscaled value of a decimal. The minimum precision must be greater than or equal to 1.

precision: 5
precision: range::[1, max]
precision: range::[min, 10]
precision: range::[1, 10]


scale: <INT>
scale: <RANGE<INT>>

An exact or minimum/maximum range indicating the number of digits to the right of the decimal point. The minimum scale must be greater than or equal to 0.

scale: 2
scale: range::[3, max]
scale: range::[min, 6]
scale: range::[3, 6]

Timestamp Constraints


timestamp_offset: [ "[+|-]hh:mm"... ]

Limits the timestamp offsets that are allowed. A offset is specified as a string of the form "[+|-]hh:mm", where hh is a two digit number between 00 and 23, inclusive, and mm is a two digit number between 00 and 59, inclusive.

timestamp_offset: ["+00:00"] // UTC
timestamp_offset: ["-00:00"] // unknown local offset
timestamp_offset: ["+07:00", "+08:00", "+08:45", "+09:00"]


timestamp_precision: <TIMESTAMP_PRECISION_VALUE>

Indicates the exact or minimum/maximum (inclusive) precision of a timestamp. Valid precision values are, in order of increasing precision: year, month, day, minute, second, millisecond, microsecond, and nanosecond.

timestamp_precision: year
timestamp_precision: microsecond
timestamp_precision: range::[month, max]
timestamp_precision: range::[min, day]
timestamp_precision: range::[second, nanosecond]
timestamp_precision: range::[month, day]
timestamp_precision: range::[year, day]

Container Constraints

The following constraints are applicable for lists, S-expressions, structs, and documents.


container_length: <INT>
container_length: <RANGE<INT>>

The exact or minimum/maximum number of elements in a list or S-expression, or fields in a struct.

container_length: 5
container_length: range::[10, max]
container_length: range::[min, 100]
container_length: range::[10, 100]


content: closed

The default behavior for containers is to allow “open” content, meaning that it is valid to provide additional elements in a list or S-expression, or fields in a struct (although such additional content might exceed a constraint and thus cause the value to be invalid for that reason). This constraint indicates that additional fields in a struct, or additional elements in a list, S-expression, or document, are not allowed.

content: closed



Defines the type and/or constraints for all values within a homogeneous list, S-expression, or struct.

element: string
element: { type: string, codepoint_length: 5 }


occurs: <INT>
occurs: <RANGE<INT>>
occurs: optional
occurs: required

Applicable only within the context of ordered_elements and struct field constraints; indicates either the exact or minimum/maximum number of occurrences of the specified type or field. The special value optional is synonymous with range::[0, 1]; similarly, the special value required is synonymous with the exact value 1 (or range::[1, 1]).

occurs: 3
occurs: range::[1, max]
occurs: range::[min, 3]
occurs: range::[1, 5]
occurs: optional           // equivalent to range::[0, 1]
occurs: required           // equivalent to 1 or range::[1, 1]

List/S-expression/Document Constraints


contains: [ <VALUE>... ]

Indicates that the list or S-expression is expected to contain all of the specified values, in no particular order.

contains: [high]
contains: [1, 4.0, high, "apple"]


ordered_elements: [ <TYPE_REFERENCE>... ]

Defines constraints over a list of values in a heterogeneous list, S-expression, or document. Each value in a list, S-expression, or document is expected to be valid against the type in the corresponding position of the specified types list. Each type is implicitly defined with occurs: 1 – behavior which may be overridden.

When specified, this constraint fully defines the content of a list, S-expression, or document – open content is not implicitly allowed.

Note that when a type in a heterogeneous list, S-expression, or document may occur some variable number of times, matching against a particular type is performed greedily before proceeding to the next type.

ordered_elements: [
  { type: int, valid_values: range::[0, 100] },
ordered_elements: [
  { type: int, occurs: range::[1, max] },      // 1..n ints

Struct Constraints


fields: { <FIELD>... }

Declares one or more field constraints of a struct, where <FIELD> is defined as:


Field names defined for a particular struct type shall be unique. A field may narrow its declared type by specifying additional constraints. By default, a field is constrained by occurs: optional.

fields: {
  city: string,
  age: { type: int, valid_values: range::[0, 200] },

Logic Constraints

The following constraints provide logical behavior over a collection of one or more types.


all_of: [ <TYPE_REFERENCE>... ]

Value must be valid for all of the types.

all_of: [


any_of: [ <TYPE_REFERENCE>... ]

Value must be valid for one or more of the types.

// valid: "hi", 0, 100, 0.0, 0e0, null, null.string
// invalid: 101,
any_of: [
  { valid_values: range::[0, 100] },


one_of: [ <TYPE_REFERENCE>... ]

Value must be valid for exactly one of the types.

// valid: "hello", 5, null, null.string
// invalid: hello, 1.3,, null.symbol
one_of: [



Value must not be valid for the type.

// valid: -1, 101, null,, "hi"
// invalid: 0, 100
not: { type: int, valid_values: range::[0, 100] }

Type Annotations

It can be helpful to tag a value with the name of the type it corresponds to, although the only way to determine whether a value corresponds to a type is to validate the value against that type. By convention, a value may be annotated as follows:


Implementation Considerations


This section attempts to shed light on some of the insights that guided key decisions when creating this specification.


This section provides a BNF-style grammar for the Ion Schema Language.


<HEADER> ::= schema_header::{
  imports: [ <IMPORT>... ]

           | <IMPORT_TYPE>
           | <IMPORT_TYPE_ALIAS>

<IMPORT_SCHEMA>     ::= { id: <ID> }

<IMPORT_TYPE>       ::= { id: <ID>, type: <TYPE_NAME> }

<IMPORT_TYPE_ALIAS> ::= { id: <ID>, type: <TYPE_NAME>, as: <TYPE_ALIAS> }

<FOOTER> ::= schema_footer::{

<TYPE_DEFINITION> ::= type::{ name: <TYPE_NAME>, <CONSTRAINT>... }
                    | { <CONSTRAINT>... }

<ID> ::= <STRING>
       | <SYMBOL>



<TYPE_REFERENCE> ::=           <TYPE_NAME>
                   | nullable::<TYPE_NAME>
                   |           <TYPE_ALIAS>
                   | nullable::<TYPE_ALIAS>
                   |           <TYPE_DEFINITION>
                   | nullable::<TYPE_DEFINITION>
                   |           <IMPORT_TYPE>
                   | nullable::<IMPORT_TYPE>

           | <FLOAT>
           | <INT>

               | <FLOAT>
               | <INT>
               | <NUMBER>

                      | range::[ min, <RANGE_TYPE> ]
                      | range::[ <RANGE_TYPE>, max ]
                      | range::[ min, max ]

               | <ANNOTATIONS>
               | <ANY_OF>
               | <BYTE_LENGTH>
               | <CODEPOINT_LENGTH>
               | <CONTAINER_LENGTH>
               | <CONTAINS>
               | <CONTENT>
               | <ELEMENT>
               | <FIELDS>
               | <NOT>
               | <OCCURS>
               | <ONE_OF>
               | <ORDERED_ELEMENTS>
               | <PRECISION>
               | <REGEX>
               | <SCALE>
               | <TIMESTAMP_OFFSET>
               | <TIMESTAMP_PRECISION>
               | <TYPE>
               | <VALID_VALUES>

<ALL_OF> ::= all_of: [ <TYPE_REFERENCE>... ]

               | required::<SYMBOL>
               | optional::<SYMBOL>

<ANNOTATIONS> ::= annotations: [ <ANNOTATION>... ]
                | annotations: required::[ <ANNOTATION>... ]
                | annotations: ordered::[ <ANNOTATION>... ]

<ANY_OF> ::= any_of: [ <TYPE_REFERENCE>... ]

<BYTE_LENGTH> ::= byte_length: <INT>
                | byte_length: <RANGE<INT>>

<CODEPOINT_LENGTH> ::= codepoint_length: <INT>
                     | codepoint_length: <RANGE<INT>>

<CONTAINER_LENGTH> ::= container_length: <INT>
                     | container_length: <RANGE<INT>>

<CONTAINS> ::= contains: [ <VALUE>... ]

<CONTENT> ::= content: closed



<FIELDS> ::= fields: { <FIELD>... }


<OCCURS> ::= occurs: <INT>
           | occurs: <RANGE<INT>>
           | occurs: optional
           | occurs: required

<ONE_OF> ::= one_of: [ <TYPE_REFERENCE>... ]

<ORDERED_ELEMENTS> ::= ordered_elements: [ <TYPE_REFERENCE>... ]

<PRECISION> ::= precision: <INT>
              | precision: <RANGE<INT>>

<REGEX> ::= regex: <STRING>
          | regex: i::<STRING>
          | regex: m::<STRING>
          | regex: i::m::<STRING>

<SCALE> ::= scale: <INT>
          | scale: <RANGE<INT>>

<TIMESTAMP_OFFSET> ::= timestamp_offset: [ "[+|-]hh:mm"... ]

                              | month
                              | day
                              | minute
                              | second
                              | millisecond
                              | microsecond
                              | nanosecond

                        | timestamp_precision: <RANGE<TIMESTAMP_PRECISION_VALUE>>


<VALID_VALUES> ::= valid_values: [ <VALUE>... ]
                 | valid_values: <RANGE<NUMBER>>


The following examples illustrate how Ion Schema concepts work together, and how they are expressed in ISL.

customer profile data



  name: short_string,
  type: string,
  codepoint_length: range::[min, 50],

  name: Address,
  type: struct,
  annotations: ordered::[one, two, three],
  fields: {
    address1: { type: short_string, occurs: required },
    address2: { type: short_string },
    city: { type: string, occurs: required, codepoint_length: range::[min, 20] },
    state: { type: State, occurs: required },
    zipcode: { type: int, valid_values: range::[10000, 99999], occurs: required },

type::{           // enum
  name: State,
  valid_values: [
    AK, AL, AR, AZ, CA, CO, CT, DE, FL, GA, HI, IA, ID, IL, IN, KS, KY,
    LA, MA, MD, ME, MI, MN, MO, MS, MT, NC, ND, NE, NH, NJ, NM, NV, NY,
    OH, OK, OR, PA, RI, SC, SD, TN, TX, UT, VA, VT, WA, WI, WV, WY



  imports: [
    { id: "com/example/util_types.isl", type: Address },

  name: Customer,
  type: struct,
  annotations: [corporate, gold_class, club_member],
  fields: {
    firstName: { type: string, occurs: required },
    middleName: nullable::string,
    lastName: { type: string, occurs: required },
    customerId: {
      type: {
        one_of: [
          { type: string, codepoint_length: 18 },
          { type: int, valid_values: range::[100000, 999999] },
      occurs: required,
    addresses: {
      type: list,
      element: Address,
      occurs: required,
      container_length: range::[1, 7],
    last_updated: {
      type: timestamp,
      timestamp_precision: range::[second, millisecond],
      occurs: required,



A union of int, string, and list<int_or_string> types:

  one_of: [
    { type: list, element: { one_of: [int, string] } },

byte_length constraint

Type corresponding to the byte_length constraint:

// byte_length: <INT>
// byte_length: <RANGE<INT>>
  name: byte_length,
  fields: {
    byte_length: {
      one_of: [
        int_non_negative,        // <INT>
        range_int_non_negative,  // <RANGE<INT>>
      occurs: required,

// range::[ <INT>, <INT> ]
// range::[ min, <INT> ]
// range::[ <INT>, max ]
// range::[ min, max ]
  name: range_int_non_negative,
  type: list,
  annotations: required::[range],
  ordered_elements: [
    { one_of: [ int_non_negative, { valid_values: [min] } ] },
    { one_of: [ int_non_negative, { valid_values: [max] } ] },
  container_length: 2,

// an int (>= 0) with optional 'exclusive' annotation
  name: int_non_negative,
  type: int,
  annotations: [exclusive],
  valid_values: range::[0, max],


The following schema provides an example of using the ‘document’ type, and illustrates what the schema for ISL might look like.


  name: IonSchema,
  type: document,
  ordered_elements: [
    { type: Header, occurs: optional },
    { type: Type,   occurs: range::[0, max] },
    { type: Footer, occurs: optional },

  name: Header,
  type: struct,
  annotations: [required::schema_header],
  fields: {
    imports: ImportList,

  name: ImportList,
  type: list,

  name: Type,
  type: struct,
  annotations: [required::type],
  fields: {
    name: symbol,

  name: Footer,
  type: struct,
  annotations: [required::schema_footer],



A parameterized list containing strings:

  name: MyListOfString,
  type: list,
  element: string,

list<bool, string, int+>

A heterogeneous list that contains a bool, string, and one or more non-negative ints:

  name: MyHeterogeneousList,
  type: list,
  ordered_elements: [
    { type: int, valid_values: range::[0, max], occurs: range::[1, max] },

map<string,int> represented as struct<int>

  name: MyMapAsStruct,
  type: struct,
  element: int,

map<string,int> represented as list<pair<string,int>>

  name: MyMapAsList,
  type: list,
  element: {
    type: list,          // pair
    ordered_elements: [