Support of traits with fixed-schema JSON (Glaze/DAW) alongside DOM types
What's your question?
Is there a support of traits w/o DOM, fixed-schema structs for headers/claims, or can such support be added?
Additional Context
To my knoledge jwt-cpp’s traits assume:
value_type≈variant<string_type, bool_type, array_type, ...>.object_type≈map<string_type, value_type>.array_type≈vector<value_type>.get_type/as_*operate onvalue_type.
This is fine with DOM libraries (nlohmann/json, jsoncons, Glaze’s glz::json_t).
However, it prevents using faster fixed-schema parsing (Glaze, DAW JSON) where you want to map payloads directly to C++ structs without constructing a DOM or converting to/from one. It also makes it harder to enforce schema strictness at parse time.
I’m most familiar with Glaze (https://github.com/stephenberry/glaze). It supports both a DOM (glz::json_t) and struct-first parsing (faster, fewer allocations).
I've tried working around, creating object_type that implements [], count(), at(), but couldn't make it out because:
is_valid_json_object/is_valid_json_arrayrequireobject_type::mapped_type == value_typeand arrays ofvalue_type. That stops us using a struct-like holder for claims, even when defining cutom struct with object_type like in jsoncons examplevalue_typeis threaded throughget_type/as_*, binding the library to DOM semantics. I cant pass one single member of struct to there, or couldn't find a way.
Why this can be usefull
- Performance: struct-first parsing is typically much faster (no DOM allocation/copies).
- Safety/strictness: schema-validated claims at parse time reduce post-parse checks and surprises.
- Ecosystem: Glaze/DAW users can keep their compile-time models while still using jwt-cpp’s signing/verification. And frankly avoiding using more then one json library in project
My naive answer is this technique for json libraries probably didn't exist when I thought of making this generic 6+ years ago. Simdjson was probably closest back then?
Your assumption is based in modern c++, variants didn't exist then in the stl and union was used by the majority. So a struct is just an object type and value type is a union of possible types.
Without looking at the code, I'd be curious if the internal type easaure for glaze would satisfy the traits if extended https://github.com/stephenberry/glaze?tab=readme-ov-file#type-support. Not sure if you're referencing that.
I'd love to see a draft pr with what you've done/tried .
One challenge with fixed structure is token format change very regularly so you will need to accept free form data when parsing. Ever server will issue tokens differently with all the optional in the various RFC.
When using Glaze with structs (not glz::json_t and its DOM internals —https://github.com/stephenberry/glaze/blob/main/include/glaze/json/json_t.hpp) it’s straightforward to wire it up to jwt-cpp like this:
using json = glz::json_t;
using value_type = json; // where glz::json_t has a field named data of type glz::json_t::val_t data which is a variant
using object_type = json::object_t;
using array_type = json::array_t;
using string_type = std::string;
using number_type = double;
using integer_type = std::int64_t;
using boolean_type = bool;
// variant as_* and get_type is straightforward, though glaze doesn't store ints only doubles
This works, but in this form it offers no real advantage over nlohmann::json.
Glaze does not type-erase struct fields.
It uses compile-time reflection and code generation: for a given struct, it generates constexpr serializers/deserializers. There’s no runtime type erasure like in a DOM — fields are read/written directly via generated code (tuples/visitation under the hood). Templates are used to select the correct function for each field type.
From reading the source, my understanding is:
- It iterates over struct fields at compile time.
- Based on the field type, the compiler builds an appropriate parser.
I may be wrong in some details, but I’m fairly confident this is correct.
As for the challenge of having fixed structure.
First solution – Optional fields in a unified struct:
Glaze (and DAW, if I’m not mistaken) supports optional fields via std::optional or glz::meta. You can define a single general struct containing all RFC-compliant fields, plus an additional map for unknown keys, inlined into the schema rather than nested JSON: Glaze: unknown keys
Second solution – Strict schema via templates: We can define our schemas for each JWT we know we'll generate
struct Headers {
std::string alg{};
};
struct Claims {
std::string our_data;
};
struct trait<Headers, Claims> {
...
};
This way we can enforce schema, in case we are both issuer and the ones who check. And usually the fields dont change as much
We can also create a operator[] for accessing by key for that trait/claims/headers, since we can retrieve the field name of a member of a struct using glaze, and c++26 reflection (in future). But I'll have to test this to be absolutely sure.
I can try creating prto support this, but I’m busy at the moment. If I get time, I’ll make an attempt — though at first glance it seems like it might require introducing a whole new jwt class to do it cleanly.
When using Glaze with structs (not
glz::json_tand its DOM internals —https://github.com/stephenberry/glaze/blob/main/include/glaze/json/json_t.hpp) it’s straightforward to wire it up to jwt-cpp like this:using json = glz::json_t; using value_type = json; // where glz::json_t has a field named data of type glz::json_t::val_t data which is a variant using object_type = json::object_t; using array_type = json::array_t; using string_type = std::string; using number_type = double; using integer_type = std::int64_t; using boolean_type = bool;// variant as_* and get_type is straightforward, though glaze doesn't store ints only doubles This works, but in this form it offers no real advantage over nlohmann::json.
Glaze does not type-erase struct fields. It uses compile-time reflection and code generation: for a given struct, it generates
constexprserializers/deserializers. There’s no runtime type erasure like in a DOM — fields are read/written directly via generated code (tuples/visitation under the hood). Templates are used to select the correct function for each field type.From reading the source, my understanding is:
- It iterates over struct fields at compile time.
- Based on the field type, the compiler builds an appropriate parser.
I may be wrong in some details, but I’m fairly confident this is correct.
As for the challenge of having fixed structure.
First solution – Optional fields in a unified struct:
Glaze (and DAW, if I’m not mistaken) supports optional fields via std::optional or glz::meta. You can define a single general struct containing all RFC-compliant fields, plus an additional map for unknown keys, inlined into the schema rather than nested JSON: Glaze: unknown keys
Second solution – Strict schema via templates: We can define our schemas for each JWT we know we'll generate
struct Headers { std::string alg{}; }; struct Claims { std::string our_data; };
struct trait<Headers, Claims> { ... }; This way we can enforce schema, in case we are both issuer and the ones who check. And usually the fields dont change as much
We can also create a
operator[]for accessing by key for that trait/claims/headers, since we can retrieve the field name of a member of a struct using glaze, and c++26 reflection (in future). But I'll have to test this to be absolutely sure.I can try creating prto support this, but I’m busy at the moment. If I get time, I’ll make an attempt — though at first glance it seems like it might require introducing a whole new jwt class to do it cleanly.
I would also like a way for force strict schema. My schemas contain mandatory and optional values, as well as certain rules, e.g. regex for some strings.
Is there a support of traits w/o DOM, fixed-schema structs for headers/claims, or can such support be added?
Without looking much into it, I assume probably not without some major restructure of the internal and potentially the external api of jwt-cpp. While I always have an open ear for anything security or performance related I am not sure if such a rewrite is a good idea given it would likely break tons of existing code. When jwt-cpp was drafted compile time reflection was a distant dream at best.
I would also like a way for force strict schema. My schemas contain mandatory and optional values, as well as certain rules, e.g. regex for some strings.
Assuming I correctly understood what you want to do you can do that already using the callback based claim verifications: https://github.com/Thalhammer/jwt-cpp/blob/cf0dab12633bb6c39ae7a7fe147b3d2fb7b3f047/include/jwt-cpp/jwt.h#L3757 jwt-cpp will invoke the function you pass to it when it tries to verify that claim and you can implement whichever validation logic you want in there.
Is there a support of traits w/o DOM, fixed-schema structs for headers/claims, or can such support be added?
Without looking much into it, I assume probably not without some major restructure of the internal and potentially the external api of jwt-cpp. While I always have an open ear for anything security or performance related I am not sure if such a rewrite is a good idea given it would likely break tons of existing code. When jwt-cpp was drafted compile time reflection was a distant dream at best.
I would also like a way for force strict schema. My schemas contain mandatory and optional values, as well as certain rules, e.g. regex for some strings.
Assuming I correctly understood what you want to do you can do that already using the callback based claim verifications:
Line 3757 in cf0dab1
/**
jwt-cpp will invoke the function you pass to it when it tries to verify that claim and you can implement whichever validation logic you want in there.
That's a shame, but I kind of expected it. I’ve looked through the code a bit, and as you mentioned, adding that would likely require a separate logic or even a new class to avoid breaking the current implementation—I don’t see a way to make it compatible otherwise. I have to say, though, parsing approaches like Glaze are noticeably faster.
I would like to see that as a feature eventually, since all compile time trickery is becoming more common, well as i see it, and c++26 will bring reflection to std so it may be even more in the future.
Though I've looked into other reflection libs and couldn't figure out any way to generalize it like its currently done with traits, but i'm sure there could be a way, i just haven't looked too hard
adding that would likely require a separate logic or even a new class to avoid breaking the current implementation
That would be a viable option though. The verifier and builder are only a small part of jwt-cpp, the majority is all the crypto algorithms. Especially given a verifier for a library like glaze would probably be a lot smaller because a lot of verification is shifted to the json library in this case. A generic api for those libraries would probably template the verifier around a header and payload class and provide methods to turn a json string into those objects. The claim verification would take a payload/header and directly access the members.
I am happy to add new features as long as they don't break api compatibility in a major way. Note that breaking ABI is fine, jwt-cpp has never guaranteed ABI stability because that's really hard to do in a header only library.