--- title: Casts and validation description: Store JSON and encrypted columns with lagoon.Jsonable and lagoon.Encrypted, and validate input with Laravel-style rule strings through lagoon.Validate. section: database order: 50 --- # Casts and validation Eloquent casts a column through `$jsonable`, `$casts` and the `encrypted` cast, and WinterCMS models validate with a `$rules` array. [lagoon](../../modules/lagoon/README.md) keeps both: column types that implement `sql.Scanner` and `driver.Valuer`, and a validator that reads the same rule strings. ## JSON columns `lagoon.Jsonable` is the Go form of `$jsonable`. It stores any Go value as JSON text in a `TEXT` column (not `jsonb`, so data copied from a WinterCMS database fits as it is). The value lives in `Data`; `Valid` tells SQL `NULL` apart from an empty value, because a nil slice and an empty one are different rows: ```go src=modules/lagoon/example_test.go#ExampleJsonable tags := lagoon.Jsonable[[]string]{Data: []string{"go", "cms"}, Valid: true} v, _ := tags.Value() fmt.Println(v) var none lagoon.Jsonable[[]string] // Valid false stores SQL NULL v, _ = none.Value() fmt.Println(v) var read lagoon.Jsonable[[]string] _ = read.Scan(`["winter"]`) fmt.Println(read.Get(), read.Valid) // Output: // ["go","cms"] // // [winter] true ``` Set `NullOnEmpty` on a slice or map column that should store `NULL` rather than `[]` or `{}` when it is empty. Pick the behaviour the PHP table already has. ## Encrypted columns `lagoon.Encrypted` is the `encrypted` cast. It stores AES-256-GCM ciphertext under a key derived from `app.key`, and decrypts values written under any key in `app.previous_keys`, so you can rotate the key without rewriting every row at once. The plaintext has one accessor, `lagoon.Encrypted.Reveal`; printing the value or marshalling it to JSON always gives `[redacted]`: ```go src=modules/lagoon/example_test.go#ExampleEncrypted // The application publishes the keys from app.key at boot; a test can // install a key directly. key := []byte("0123456789abcdef0123456789abcdef") if err := lagoon.PublishEncryptionKeys(nil, key, nil); err != nil { fmt.Println(err) return } token := lagoon.NewEncrypted("s3cret") stored, _ := token.Value() // what the column holds fmt.Println(strings.Contains(fmt.Sprint(stored), "s3cret")) var read lagoon.Encrypted if err := read.Scan(stored); err != nil { fmt.Println(err) return } out, _ := json.Marshal(map[string]any{"api_token": read}) fmt.Println(read, string(out)) fmt.Println(read.Reveal()) // Output: // false // [redacted] {"api_token":"[redacted]"} // s3cret ``` The application loads the keys once at boot (`lagoon.OpenFromApp` calls `lagoon.LoadAppKey` and `lagoon.PublishEncryptionKeys`). A missing or short `app.key` stops the application with an error; there is no default key. Generate one with `key:generate`, which prints a key and writes nothing: ```sh ./bin/acme key:generate ``` Keep the key in the environment (`SUMMER_APP__KEY`), not in a committed file. The ciphertext format is not Laravel's. To import rows that the PHP application encrypted, decrypt them once with `lagoon.DecryptLaravelPayload` and the old `APP_KEY`, then save them through `lagoon.Encrypted`. It is meant for a one-off import, never for reading live data. ## Validation `lagoon.Validate` checks a map of input values against Laravel-style rule strings and returns the errors in Laravel's shape, a map from field to messages. It returns `nil` when the input is valid. The second return value is for failures that are not the user's fault, such as an unknown rule or a database error: ```go src=modules/lagoon/example_test.go#ExampleValidate rules := map[string]string{ "title": "required|max:10", "views": "nullable|integer|max:1000", } input := map[string]any{"title": "", "views": 5000} // A nil translator gives the built-in English messages; unique: rules // need a database handle instead of nil. errs, err := lagoon.Validate(context.Background(), nil, &Post{}, rules, input, nil) if err != nil { fmt.Println(err) } out, _ := json.Marshal(errs) fmt.Println(string(out)) // Output: // {"title":["The title field is required."],"views":["The views may not be greater than 1000."]} ``` The supported rules are `required`, `nullable`, `integer`, `numeric`, `between`, `min`, `max`, `in`, `unique`, `boolean`, `email`, `confirmed`, `different` and `mimes`. Any other rule is an error, so a rule that SummerCMS does not implement cannot be skipped by accident. On a field that is `integer` or `numeric`, `min`, `max` and `between` compare the number; on other fields they compare the length. > [!NOTE] > A failed numeric range check is currently always reported with the `max` message, even when the value is below `min`, and a `min`-only rule then shows an empty limit. Check range errors by field, not by message text. `unique:` runs a query to check that no other row of the table has the value in the field's column, so it needs a database handle; pass the transaction you are writing in. Soft-deleted rows do not count, and when the model you pass has an ID, its own row does not count either, so the same rules work for create and update. Pass a `phrasebook.Translator` as the last argument to get the messages in the request locale from the `lagoon::validate` catalog; with `nil` they are in English. See [Localization](../services/localization.md). A handler validates before it fills and saves the model, as the create example on [Models](models.md) shows. Answer a non-nil error map with status 422 and the body shape the endpoint's existing clients expect.