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 any_of: nullable::[int, string, struct] is expected, 5, "hi", {}, null, null.null, null.int, 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:

$ion_schema_1_0

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

<NAMED_TYPE_DEFINITION>...

schema_footer::{         // optional
}

Imports

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. Only named, top-level types of a schema may be imported in another schema.

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 therefore 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:

type::{
  name: <TYPE_NAME>,
  <CONSTRAINT>...
}

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::{
  type: list,              // type reference
  element: {               // inline type
    type: string,
    codepoint_length: 10,
  },
}

An inlined type definition may also contain an occurs field that indicates how many times a value of a specific type may occur. This is applicable only within the context of ordered_elements and struct fields constraints.

// Field 'foo' is required
type::{
  fields: {
    foo: {                // inline type with `occurs`
      occurs: required,
      type: string,
      codepoint_length: range::[1, max],
    },
  }
}

type::{
  type: sexp,
  ordered_elements: [
    {
      valid_values: ['+', '*', '-', '/', '^'] 
    },
    {
      occurs: 2,
      type: number,
    },
  ]
}

The occurs field 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]

The default value for occurs is specific to each constraint; refer to fields and ordered_elements for more details.

Open Content

The default behavior for structs is to allow additional content beyond what is explicitly specified for a given type; this is referred to as open content. For a given struct that is not constrained by content: closed, it may contain additional fields 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

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.

Ranges

range::[ <EXCLUSIVITY><RANGE_TYPE>, <EXCLUSIVITY><RANGE_TYPE> ]
range::[ min, <EXCLUSIVITY><RANGE_TYPE> ]
range::[ <EXCLUSIVITY><RANGE_TYPE>, 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. A range may not contain both min and max.

All ranges have a type. The type of the range is the same as that of the minimum and/or maximum values specified in the range list. If both a minimum and maximum values are specified (i.e. min and max are not used), then both of those values must be of the same type. (For example, range::[1995-12-06T, 55.4] mixes values of the timestamp and number types, and therefore is not a valid range.)

RANGE<NUMBER>

A number range includes all values of any numeric type (i.e. float, int, and decimal) that fall within that range mathematically. When testing a value for inclusion, the range bounds and the value that is being tested will all be converted to decimal values for the sake of comparison. number ranges do not include values of any other type.

The float type includes special non-number values (nan, +inf, and -inf). Attempting to use a non-number value as the bound of a number range will result in an error. When tested for inclusion in a number range, the non-number float values are always outside of a number range, even when one of the range bounds is min or max.

RANGE<TIMESTAMP>

All timestamp values represent an instant in time. A timestamp with limited precision (for example, a year-only timestamp like 2007T) maps to an instant by assuming that all unspecified time unit fields are effectively zero (or one for the month and day units). 2007T would map to the instant represented by 2007-01-01T00:00.000-00:00. Note that timestamp values that do not have a time component (that is: YYYYT, YYYY-MMT, and YYYY-MM-DDT timestamps) have an unknown offset (-00:00). Timestamps that have an unknown offset are UTC timestamps that make no assertion about the offset in which they occurred.

 // Timestamp                  Instant
    2007T                      2007-01-01T00:00.000-00:00
    2007-05T                   2007-05-01T00:00.000-00:00
    2007-05-23T                2007-05-23T00:00.000-00:00
    2007-05-23T:06:15Z         2007-05-01T06:15.000Z
    2007-05-23T:06:15-00:00    2007-05-01T06:15.000-00:00
    2007-05-23T:06:15+00:00    2007-05-01T06:15.000+00:00
    2007-05-23T:06:15+05:00    2007-05-01T06:15.000+05:00
    2007-05-23T:06:15.945Z     2007-05-01T06:15.945Z   

A timestamp range includes any timestamp that falls between the minimum and maximum in chronological order. timestamp ranges do not include values of any other type.

range::[5, max]                               // minimum 5, maximum unbound
range::[min, 7]                               // minimum unbound, maximum 7
range::[5, 7]                                 // between 5 and 7, inclusive
range::[1.0, 10e]                             // mixing numeric types is allowed
range::[exclusive::5, exclusive::7]           // between 5 and 7, exclusive; if type is also constrained to be an int, only 6 is allowed
range::[5.5, 7.9]                             // between 5.5 and 7.9, inclusive
range::[2019-01-01T, exclusive::2020-01-01T]  // any timestamp in the year 2019

General Constraints

annotations

annotations: [ <ANNOTATION>... ]
annotations: required::[ <ANNOTATION>... ]
annotations: closed::[ <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.

The required, closed, and ordered annotations 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. Additional annotations can only be constrained by adding the closed annotation to the list of valid annotations.

Annotations are only applicable to Ion values. A document is not an Ion value, but rather a stream of values, so a document is always invalid for the annotations constraint.

annotations: [red, required::green, blue]
annotations: required::[red, optional::green, blue]
annotations: required::ordered::[one, optional::two, three]
annotations: closed::required::[red, green, blue] // Annotations must contain exactly "red", "green", and "blue" in any order
annotations: closed::ordered::[red, green, blue] // Annotations must contain exactly "red", "green", and "blue" in that order
annotations: closed::[red, blue] // Only the annotations "red" and "blue" are permitted, but they are not required
annotations: closed::[] // No annotations are permitted

type

type: <TYPE_REFERENCE>
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 null.int are valid, but null.string is not).

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

valid_values

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

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.

For example, 1.230 is not valid for valid_values: [1.23], as it has a different precision. While these may be mathematically equal values, they are not equivalent data. In particular, note that nan is valid for valid_values: [nan]. In the Ion data model all valid Ion encodings of nan are equivalent data.

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.

Note that valid_values only matches a single Ion value. A document is not an Ion value, but rather a stream of values, so a document is always invalid for the valid_values constraint.

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, null.int]
valid_values: [range::[exclusive::0, max], +inf]

Blob/Clob Constraints

byte_length

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

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

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
. any codepoint
[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
{x} exactly x occurrences
{x,} at least x occurrences
{x,y} at least x and at most y occurrences

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

The following classes are provided:

   
\d digit: [0-9]
\D non-digit
\s whitespace: [ \f\n\r\t]
\S non-whitespace
\w word character: [A-Za-z0-9_]
\W non-word character

The following characters may be escaped with a backslash: . ^ $ | ? * + \ [ ] ( ) { }. Note that in Ion text a backslash must itself be escaped, so correct escaping of these characters requires two backslashes, e.g.: \\..

regex: "M(iss){2}ippi"
regex: i::"susie"
regex: i::m::"^B[0-9]{9}$"
regex: "\\$\\d+\\.\\d\\d"

utf8_byte_length

utf8_byte_length: <INT>
utf8_byte_length: <RANGE<INT>>

An exact or minimum/maximum indicating number of bytes in the UTF8 representation of a string or symbol.

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

Decimal Constraints

precision

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

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

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

Limits the timestamp offsets that are allowed. An 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: <TIMESTAMP_PRECISION_VALUE>
timestamp_precision: <RANGE<TIMESTAMP_PRECISION_VALUE>>

Indicates the exact or minimum/maximum 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::[exclusive::second, max]  // any timestamp with fractional seconds is allowed
timestamp_precision: range::[exclusive::second, exclusive::millisecond]  // only timestamps with a precision of tenths or hundredths of a second are allowed
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

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

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

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

contains

contains: [ <VALUE>... ]

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

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

element

element: <TYPE_REFERENCE>

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

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

List/S-expression/Document Constraints

ordered_elements

ordered_elements: [ <VARIABLY_OCCURRING_TYPE>... ]

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.

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

Struct Constraints

fields

fields: { <FIELD>... }

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

<SYMBOL>: <VARIABLY_OCCURRING_TYPE>

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] },
}

content

content: closed

The default behavior for structs is to allow “open” content, meaning that it is valid to provide additional 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 are not allowed.

content: closed

Logic Constraints

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

all_of

all_of: [ <TYPE_REFERENCE>... ]

Value must be valid for all of the types.

all_of: [
  LineItems,
  ShippingAddress,
  BillingAddress,
]

any_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, null.int
any_of: [
  nullable::string,
  { valid_values: range::[0, 100] },
]

one_of

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.int, null.symbol
one_of: [
  nullable::string,
  int,
]

not

not: <TYPE_REFERENCE>

Value must not be valid for the type.

// valid: -1, 101, null, null.int, "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:

<ID>::<TYPE_NAME>::<VALUE>

Rationale

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

Grammar

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

<SCHEMA> ::= <NAMED_TYPE_DEFINITION>...
           | <HEADER> <NAMED_TYPE_DEFINITION>... <FOOTER>

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

<IMPORT> ::= <IMPORT_SCHEMA>
           | <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::{
}

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

<UNNAMED_TYPE_DEFINITION> ::= type::{ <CONSTRAINT>... }
                            | { <CONSTRAINT>... }

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

<TYPE_ALIAS> ::= <SYMBOL>

<TYPE_NAME> ::= <SYMBOL>

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

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

<VARIABLY_OCCURRING_TYPE_REFERENCE> ::= type::{ <OCCURS>, <CONSTRAINT>... }
                                      | { <OCCURS>, <CONSTRAINT>... }
                                      | <TYPE_REFERENCE>

<NUMBER> ::= <DECIMAL>
           | <FLOAT>
           | <INT>

<RANGE_TYPE> ::= <DECIMAL>
               | <FLOAT>
               | <INT>
               | <NUMBER>
               | <TIMESTAMP>
               | <TIMESTAMP_PRECISION_VALUE>

<EXCLUSIVITY> ::= exclusive::
                | ""

<RANGE<RANGE_TYPE>> ::= range::[ <EXCLUSIVITY><RANGE_TYPE>, <EXCLUSIVITY><RANGE_TYPE> ]
                      | range::[ min, <EXCLUSIVITY><RANGE_TYPE> ]
                      | range::[ <EXCLUSIVITY><RANGE_TYPE>, max ]

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

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

<ANNOTATION> ::= <SYMBOL>
               | required::<SYMBOL>
               | optional::<SYMBOL>

<ANNOTATIONS_MODIFIER> ::= required::
                         | ordered::
                         | closed::

<ANNOTATIONS> ::= annotations: <ANNOTATIONS_MODIFIER>... [ <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

<ELEMENT> ::= element: <TYPE_REFERENCE>

<FIELD> ::= <SYMBOL>: <VARIABLY_OCCURRING_TYPE_REFERENCE>

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

<NOT> ::= not: <TYPE_REFERENCE>

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

<ORDERED_ELEMENTS> ::= ordered_elements: [ <VARIABLY_OCCURRING_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"... ]

<TIMESTAMP_PRECISION_VALUE> ::= year
                              | month
                              | day
                              | minute
                              | second
                              | millisecond
                              | microsecond
                              | nanosecond

<TIMESTAMP_PRECISION> ::= timestamp_precision: <TIMESTAMP_PRECISION_VALUE>
                        | timestamp_precision: <RANGE<TIMESTAMP_PRECISION_VALUE>>

<TYPE> ::= type: <TYPE_REFERENCE>

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

All Ion Schema Language keywords SHALL use Snake Case, and SHALL begin with $ or an alphabetic character.

Examples

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

customer profile data

com/example/util_types.isl:

$ion_schema_1_0

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

type::{
  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
  ],
}

com/example/customer.isl:

$ion_schema_1_0

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

type::{
  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,
    },
  },
}

schema_footer::{
}

union

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

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

byte_length constraint

Type corresponding to the byte_length constraint:

// byte_length: <INT>
// byte_length: <RANGE<INT>>
type::{
  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 ]
type::{
  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
type::{
  name: int_non_negative,
  type: int,
  annotations: [exclusive],
  valid_values: range::[0, max],
}

document

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

$ion_schema_1_0

schema_header::{
}

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

type::{
  name: SchemaVersionMarker,
  valid_values: [$ion_schema_1_0]
}

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

type::{
  name: ImportList,
  type: list,
  // ... etc. etc. 
}

type::{
  name: Type,
  type: struct,
  annotations: [required::type],
  fields: {
    name: symbol,
    // ... etc. etc.
  },
}

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

schema_footer::{
}

list<string>

A parameterized list containing strings:

type::{
  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:

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

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

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

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

type::{
  name: MyMapAsList,
  type: list,
  element: {
    type: list,          // pair
    ordered_elements: [
      string,
      int,
    ],
  },
}