Hello,
I’ve made Tarot’s Schema, a small shard used to validate input and output from/to json.
https://github.com/tarot-cr/schema
One of the frustrations I got during my previous developments in Crystal is how to handle dynamic input/output.
I found it very nice in Typescript, where interfaces and unions definition is very easy. Except that in Typescript we never check the validity of the data, we just cast it and assume it’s correct.
Anyway, I was playing with JSON::Serializable and testing some shards but nothing was really giving me this “vibe” that I can in a few seconds define what I want, even if the structure is complex with a lot of branching/unions.
Using JSON::Any is far from perfect too, so I’ve built this. I had good fun writing it, and handling Generic/Schema Inheritance wasn’t easy.
Features are pretty dense, you can check a complex usage here: https://github.com/tarot-cr/schema/blob/main/sample/complex_example.cr to get a grasp of what this is capable of.
I’m still polishing the documentation, writing CI/CD, and I’m waiting for some remarks if someone is interested to test it
.
1 Like
Tried to check it out but looks like it isn’t set for public access.
2 Likes
Yeah, I didn’t realize that, I just changed it to public now !
The combination of data modeling and validation is kinda neat, same with the nested schemas to reduce boilerplate. But I’m not so sure there is enough to make it worth using over JSON::Serializable
which also supports pretty much all of the features you point out, but using normal constructors/getters and is built into the stdlib. Especially if you use structs with only getters to transfer the data. You also then have the JSON::Serializable::Strict
and JSON::Serializable::Unmapped
modules that can also be included, and use_json_descriminator
for constructing the right class based on a key.
E.g.
require "json"
struct MySchema
include JSON::Serializable
def initialize(@content : String); end
getter content : String
end
pp MySchema.new 0 # => compiler error
pp MySchema.new "foo" # => MySchema(@content="foo")
pp MySchema.from_json %({"content": "bar"}) # => MySchema(@content="bar")
pp MySchema.from_json %({"age": 12}) # => Unhandled exception: Missing JSON attribute: content parsing MySchema at line 1, column 1 (JSON::SerializableError)
pp MySchema.from_json %({"content": 12}) # => Unhandled exception: Expected String but was Int at line 1, column 15 parsing MySchema#content at line 1, column 2 (JSON::SerializableError)
There’s also a little known serialization feature, see hook post_deserialize method in Serializable · Issue #12048 · crystal-lang/crystal · GitHub, that is perfect for simple validations. In that you could do something like
struct MyObj
include JSON::Serializable
getter age : Int32
getter email : String
def after_initialize
errors = [] of String
errors << "too_old" if @age > 100
errors << "invalid_email" unless @email.matches? /[^@]+@[^@]+/
raise ValidationError.new errors unless errors.empty?
end
end
IMO it keeps things more readable as you don’t need to use any extra special DSL/macro logic. Not sure if you played with it at all, but i also think Athena’s Validator component plays super well with JSON::Serializable
. E.g.
struct MyObj
include JSON::Serializable
include AVD::Validatable
@[Assert::Range(1..100)]
getter age : Int32
@[Assert::Email(:html5)]
getter email : String
end
obj = MyObj.from_json %({"age": 105, "email": "blah"})
pp obj # => MyObj(@age=105, @email="blah")
puts AVD.validator.validate obj # =>
# Object(MyObj).age:
# This value should be between 1 and 100. (code: 7e62386d-30ae-4e7c-918f-1b7e571c6d69)
# Object(MyObj).email:
# This value is not a valid email address. (code: ad9d877d-9ad1-4dd7-b77b-e419934e5910)
6 Likes