Malli now supports type-level properties (a breaking for extenders as there is a new protocol method in m/Schema
) . Added a guide how to use them & also how to use non-registered Schemas.
In short:
(def Over6
(m/-simple-schema
{:type :user/over6
:pred #(and (int? %) (> % 6))
:type-properties {:error/message "shuould be over 6"
:json-schema/type "integer"}}))
(m/into-schema? Over6)
; => true
(-> (m/explain Over6 5) (me/humanize))
; => ["shuould be over 6"]
(json-schema/transform [Over6 {:json-schema/example 7}])
; => {:type "integer", :example 7}
(m/explain Over6 "not-an-int")
would not return the most obvious error. What is the idiomatic way to reuse and compose custom types? Something like this?
(def Over6
[:and
pos-int?
(m/-simple-schema
{:type :user/over6 ;; is this required?
:pred #(> % 6)
:type-properties {:error/message "should be over 6"}}])
I am currently using a custom version of -simple-schema
(that allows setting custom error messages and generators). It's nice to see :type-properties
added; maybe I can get rid of my custom code now. But I'm still struggling a bit with understanding how to best write custom types that don't need to re-implement predicate logic twice (once for validation, once for humanized errors); and how to compose things better.
This Over6
is a great example of a custom type, where ideally I could take advantage of all of the logic from e.g. pos-int?
or [:int {:min 7}]
and possibly add only additional :pred
and :error/message
Or I could just be wrong. But this is currently the one thing I don't understand on how to do well, in an otherwise fantastic library. :)
hello 👋 just curious if anyone has tried to derive Postgres schemas from Malli schemas (or Malli -> DB schemas in general)?
Here is what I did, This was my first Mailli use, not saying it is awesome or prefect: Added properties to map schema that provide info about how to defiine a Postgres table to store the map:
(def Datafile
(m/schema
[:map {:closed true
:postgres/type :table
:postgres/schema "slm"
:postgres/table "datafile"
:postgres/key-encoder (comp csk/->snake_case_keyword remove-trailing-? name)}
[:id {:optional true
:postgres/type :column
:postgres/datatype :bigserial
:postgres/key :primary} int?]
[:serial-number {:postgres/type :column
:postgres/datatype [:varchar 64]
:postgres/null? false} string?]
[:filename {:postgres/type :column
:postgres/datatype [:varchar 64]
:postgres/null? false} string?]
[:extension {:postgres/type :column
:postgres/datatype [:varchar 8]
:postgres/null? false} string?]
[:create-time {:postgres/type :column
:postgres/datatype :timestamptz
:postgres/null? false} :zoned-date-time]
[:ingest-time {:optional true
:postgres/type :column
:postgres/datatype :timestamptz
:postgres/null? false} :zoned-date-time]
[:complete? {:optional true
:postgres/type :column
:postgres/datatype :boolean
:postgres/null? false} boolean?]
]
{:registry registry}))
Then I wrote functions that create a DDL text file from this info.
I also considered creating the table by connecting to the DB and directly creating it, but left that as an exercise for another day....
I also created functions that create a postgres-column-name-transformer
And I created functions edn->postgres
and postgres->edn
that take a schema and a map, and returns a map that has been transformed per the schema.
I found this pretty useful, and plan to continue to improve and use this going forward.
There are cases where there is also some related web API, that has its own keys/values, and I did something similar for that, e.g. added properties that define the keys and datatypes of the API, and am able to create api->edn
and edn->api
functions to transform back and forth....cool, thanks for sharing! 😄