# Describing Events with Schemas

Every event type has a JSON schema, which the database checks every event of the type against, once the schema is registered (see **[Registering Event Schemas](/docs/architecturekit/v1.0/registering-event-schemas)**).

The kit derives it from the struct, so that it describes exactly what `encoding/json` writes for the event:

- A struct is an object with the fields `encoding/json` writes: named by their `json` tags, without fields tagged `-` and unexported ones, and with the fields of embedded structs in place of the embedded struct. Fields with `omitempty` or `omitzero`, and fields of an embedded pointer, are optional, all others are required, and no other fields are allowed.
- A `string` is a string, a `bool` a boolean, an `int` or any other integer type an integer, and a `float64` or a `float32` a number. The `string` option of a `json` tag turns such a field into a string.
- A slice is an array, a `[]byte` a string, and an array an array of exactly its length. A map is an object whose values all have the same schema.
- A pointer, a slice and a map may also be `null`, since `encoding/json` writes `null` for `nil`, unless a field of such a type is optional and therefore left out instead.
- A `time.Time` is a string in the `date-time` format, a `json.Number` a number, unless the `string` option makes it a string, and a type with a `MarshalText` or an `AppendText` function a string. An interface allows any value, even if it declares a `Schema` function. So does a `json.RawMessage`, which is the same type as `jsontext.Value` of `encoding/json/v2`, since `encoding/json` writes the JSON it holds, or `null` if it is `nil`.

For `BookBorrowed`, this yields the following schema:

```json
{
  "type": "object",
  "properties": {
    "borrowedBy": { "type": "string" },
    "borrowedUntil": { "type": "string" }
  },
  "required": ["borrowedBy", "borrowedUntil"],
  "additionalProperties": false
}
```

These rules do not change, since a registered schema can not change either.

*Note that EventSourcingDB currently reads every number in the data of an event as a 64-bit floating-point number, which holds an integer exactly only if its magnitude is at most 2^53 - 1, which is 9007199254740991. A larger integer is stored changed, without an error: 9007199254740993 comes back as 9007199254740992, and a time in nanoseconds, as `UnixNano` returns it, loses its last digits. Close to the largest `int64` or `uint64`, the stored number may even lie beyond the range of the type, so that the event can not be read back any more, and `Load`, `Execute`, and a projection fail with an error of the category `ErrPermanent`. To keep a 64-bit integer exact, give its field the `string` option of the `json` tag, as in `json:"atNanos,string"`, which writes it as a string.*

To constrain a value further than its Go type does, declare a type for it with a `Schema` function, which returns the JSON schema of the type. Wherever a field has that type, the derived schema takes it over. For example, `borrowedUntil` accepts any string so far. To make sure that it is a date, `BookBorrowed` could use a `Date` type for the field:

```go
type Date string

func (Date) Schema() map[string]any {
  return map[string]any{"type": "string", "format": "date"}
}

type BookBorrowed struct {
  BorrowedBy    string `json:"borrowedBy"`
  BorrowedUntil Date   `json:"borrowedUntil"`
}
```

*Note that the other examples in this documentation keep `BorrowedUntil` a `string`. With the `Date` type, they would convert between the two, as in `Date(cmd.BorrowedUntil)`.*

Apart from `time.Time`, `json.Number` and `json.RawMessage`, a type that encodes itself with a `MarshalJSON` function, or with `MarshalJSONTo` of `encoding/json/v2`, which `encoding/json` calls as well, needs such a `Schema` function, too, since the kit can not know what the function writes. So does a type that has its `MarshalText` or `AppendText` function on a pointer receiver only: `encoding/json` calls it only for a value it can take the address of, so whether such a value is written as a string depends on where it is. If the schema of an event can not be derived, for example because of such a type, a recursive type, a channel, a function, or a complex number, `Evolve` or `Ignore` panics and names the field.

*Note that the derived schema never takes over the `Schema` function of an interface. `encoding/json` writes the value a field of the interface holds, or `null`, so there is no value whose `Schema` function could be asked, and the field is described as if the interface had none. To constrain such a field, or one whose interface encodes itself with `MarshalJSON`, give the event a `Schema` function of its own.*

*Note that a field of type `json.RawMessage` is constrained the same way, with a `Schema` function of the event. A type declared from `json.RawMessage`, as in `type Details json.RawMessage`, does not help, since it does not take over the `MarshalJSON` function: `encoding/json` writes such a value as a `[]byte`, which is a string.*

If an event needs a schema that its fields can not express, give the event itself a `Schema` function. It takes precedence over the derived schema. To start from the derived schema, call the `DeriveSchema` function, which derives the schema of a type without calling its own `Schema` function. For example, to require at least one of two optional fields:

```go
type BookCorrected struct {
  Title  string `json:"title,omitempty"`
  Author string `json:"author,omitempty"`
}

func (BookCorrected) EventType() string {
  return "io.eventsourcingdb.library.book-corrected"
}

func (BookCorrected) Schema() map[string]any {
  schema := architecturekit.DeriveSchema[BookCorrected]()
  schema["minProperties"] = 1

  return schema
}
```

`DeriveSchema` returns a new schema on every call, with objects as `map[string]any`, arrays as `[]any`, and numbers as `float64`, such as the `minItems` of an array, as `encoding/json` decodes them.

A `Schema` function describes the type that declares it. Go also promotes it to a struct that embeds the type, but there it still describes the embedded type alone, while `encoding/json` writes the fields of the embedded struct next to the other fields of the struct. So if an event, or the type of a field, has its `Schema` function only from an embedded field, `Evolve` or `Ignore` panics and names the embedded field, and so does `DeriveSchema` for such a type. Give the struct a `Schema` function of its own, or make the embedded type a named field.

*Note that the derived schema describes the fields of an embedded struct by their own types, as it describes all other fields, even if the embedded struct has a `Schema` function. A struct that starts its own `Schema` function from `DeriveSchema` therefore does not get the constraints from the `Schema` function of the embedded struct. Add them there if the struct needs them.*

*Note that a json tag name that `encoding/json` considers invalid also makes `Evolve` or `Ignore` panic, since `encoding/json` reads such a name differently depending on the Go version the application declares.*

## Keeping Schemas in Files

An event's own schema does not have to be written in Go. To keep it in a JSON file instead, embed the file with the `embed` package, and decode it in the `Schema` function of the event:

```go
import (
  _ "embed"
  "encoding/json"
)

//go:embed schemas/book-acquired.json
var bookAcquiredSchema []byte

func (BookAcquired) Schema() map[string]any {
  var schema map[string]any
  if err := json.Unmarshal(bookAcquiredSchema, &schema); err != nil {
    panic(err)
  }

  return schema
}
```

The `Evolve` and `Ignore` functions call `Schema` when the state is built, so a file that does not contain valid JSON stops the application on start, before it writes anything.
