J5 schemas define objects and fields, similar to JSON Schema and 'pure proto' (not gRPC).
The meta-structure of schemas is defined as proto files in the j5.schema.v1 package.
Schema definitions which can be registered directly to a package, independently to an object or wrapper type.
Root types may also be nested within Objects and Oneofs, and inline with field definitions.
A set of named fields (properties), each with its own data type and other annotations.
object Foo {
field fooId key:id62 {
| The primary key of Foo
required = true
}
field name string
}
message Foo {
string foo_id = 1 [
(buf.validate.field) = {
required: true
string: {
pattern: "^[0-9A-Za-z]{22}$"
}
},
(j5.ext.v1.field).key = {}
];
string name = 2;
}
Like an object, but at most one key can be set at a time, and all of the properties must be objects.
oneof Foo {
option bar object {
field barId key:id62
}
option baz object {
field bazId key:id62
}
}
message Foo {
oneof type {
Bar bar = 1;
Baz baz = 2;
}
message Bar {
...
}
message Baz {
...
}
}A 'oneof' notation in proto is a validation rule, the fields belong on the parent message, where this proto structure uses a message to wrap oneof into being a unique type, allowing it to be reused, and for code generation to add methods to it.
Oneofs are encoded as a JSON object, with a !type field, and the single key
matching that type as a sub-object.
{
"!type": "bar",
"bar": {
"barId": "123"
}
}A set of named values.
enum Status {
option ACTIVE
option INACTIVE
}
These map to proto enums following the Buf rules.
enum Status {
STATUS_UNSPECIFIED = 0;
STATUS_ACTIVE = 1;
STATUS_INACTIVE = 2;
}The first proto enum value will always be {prefix}_UNSPECIFIED, it can be
omitted from the source, or explicitly included (as UNSPECIFIED) to add
descriptions or other extensions to the option.
enum Status {
option UNSPECIFIED | Initial Status
option ACTIVE
}
enum Status {
// Initial Status
STATUS_UNSPECIFIED = 0;
STATUS_ACTIVE = 1;
}JSON encoding uses the shorter string (e.g. ACTIVE rather than
STATUS_ACTIVE) but will decode either form.
TODO: Clarify unspecified as '', null or omitted, in both the implementation and the docs
A polymorph is a merge between a oneof and an any field.
The value of the any is constrained to the listed types on the polymorph.
It allows for reverse-imports where types are known in advance, but the members are in packages which import and use the definition.
The proto implementation is a message with a single 'value' field, which is a j5.types.any.v1
The listed types must be fully qualified type names, they may be in the same package or in a separate package, however if they are in a separate package, the packages they belong to cannot be imported directly or indirectly by the package containing the polymorph.
The implementations of the polymorph must also mark themselves as such, which does require an import.
The implementations are checked against the polymorph's type list to ensure they are listed.
package foo.v1
polymorph FooMorph {
member bar.v1.Bar
member baz.v1.Baz
}
package bar.v1
import foo.v1 as foo
object Bar {
polymorphMember foo.FooMorph
field barId key:id62
}
Fields in an object or oneof are defined by j5.schema.v1.ObjectProperty.
In J5s files, each field must have a name and type, defined by the tags in the syntax.
field name string
Some types require further qualification, such as the key type, which requires
a key type, such as id62 or uuid, or integer types, which require a bit
width and signedness, and object, oneof and enum require the type name.
field fooId key:id62
field age integer:INT32
field bar object:Bar
For further attributes, the field can have a 'body', using curly braces:
field name string {
required = true
}
All object properties have two common attributes - required and
explicitlyOptional. A shortcut for these is to add a ! or ? respectively before the
data type in the definition:
field name ! string
is equivalent to
field name string {
required = true
}
All other attributes are defined by the specific data type of the field, for
example, the key type has primary (bool) and foreign (string) attributes.
field fooId key:id62 {
required = true
foreign = "bar.v1.Bar"
}
When a field is an object type, the field can have the flatten = true
attribute, which makes the JSON encoding of the inner object act as fields of
the outer object. This can be used to create an 'extends' sort of pattern.
When a field references a root type, the type can be defined
either as a reference object:Bar or inline:
object Foo {
field bar object {
field barId key:id62
}
}
message Foo {
Bar bar = 1;
message Bar {
...
}
}This can also be used when the type is an array or map of an object, oneof or enum.
object Foo {
field bars array {
field barId key:id62
}
}
message Foo {
repeated Bar bars = 1;
message Bar {
...
}
}The inline type by default will take the name of the field, but can be overridden by directly accessing the name property:
object Foo {
field bars array:object {
object.name = "Bar"
field barId key:id62
}
}
message Foo {
repeated Bar bars = 1;
message Bar {
...
}
}Arrays and Maps are defined as fields with a type of array or map as a
prefix to the sub-type.
- The sub-type can be anything other than a map or array. (constraint carried over from proto)
- Map keys are strings, as in JSON.
field names array:string
field ages map:integer:INT32
repeated string names = 1;
map<string, int32> ages = 2;| J5 Type | Proto Type | JSON Type |
|---|---|---|
| string | string | string |
| bool | bool | bool (true,false) |
| integer:INT32 | int32 | unquoted literal |
| integer:INT64 | int64 | quoted string |
| integer:UINT32 | uint32 | unquoted literal |
| integer:UINT64 | uint64 | quoted string |
| float:FLOAT32 | float | unquoted literal |
| float:FLOAT64 | double | unquoted literal |
| bytes | bytes | base64 std string |
| timestamp | google.protobuf.Timestamp | RFC3339 string |
| date | j5.types.date.v1.Date | string "YYYY-MM-DD" |
| decimal | j5.types.decimal.v1.Decimal | quoted string |
| key | string (with annotation) | string |
'quoted literal' means a numerical string with quotes, e.g. "123", and
'unquoted literal' means a numerical string without quotes, e.g. 123.
The J5 Codec translates between JSON and Proto representations. It produces the representations above, but accepts a more flexible range of inputs:
- All number types (ints, floats, decimal) can be represented as a quoted or unquoted
- Base64 can be encoded with either URL or Standard encoding, with or without padding