Skip to content

Added support for boolean combination keywords #7

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 16, 2025

Conversation

NickSale
Copy link

Was wanting to use the JSON boolean combination keywords (https://json-schema.org/understanding-json-schema/reference/combining#boolean-json-schema-combination) in a schema but still have the schema automatically generated.

Main changes:

  • Added new types AllOf{T,S}, AnyOf{T,S}, OneOf{T,S}, Not{T} whose use in a struct allows the schema generator to add the corresponding keywords
  • Made it so that the schema generator will interpret Val types in structs as JSON const values (e.g. x::Val{true} -> x : { const : true })

Included an extra test making use of these changes.

Let me know if you think there's a nicer way to do this!

@MichaelHatherly
Copy link
Contributor

This would be really nice to have, I've got some local hacks to do similar for anyOf, but they're definitely not generally useful or worth sharing yet.

How does this integrate with JSON3.read/JSON3.write? Can you (de)serialise into types that make use of these new structs you've defined here?

@NickSale
Copy link
Author

How does this integrate with JSON3.read/JSON3.write? Can you (de)serialise into types that make use of these new structs you've defined here?

Good question!
For write: as presented, you end up with something along the lines of "JSG.AllOf{JSG.AnyOf{ConstantInt1Schema, ConstantInt2Schema}, JSG.Not{ConstantBoolTrueSchema}}()" in your output JSON string which is not very helpful.
For read: providing a default value for allOf, etc in your struct avoids the need to have such an element in your input JSON.

I think the best bet would be to add allOf, etc to StructTypes.excludes, avoiding writing and reading the element at all. You only really want it to show up in the generated schema.

@NickSale
Copy link
Author

NickSale commented May 13, 2025

How does this integrate with JSON3.read/JSON3.write? Can you (de)serialise into types that make use of these new structs you've defined here?

Ah now I realise you perhaps mean deserializing JSON that has these keywords into automatically generated types that use these AllOf{T,S} etc structs? That's not something I've really thought about, although I would guess it would be fairly straightforward? But my use case was only for the reverse problem (types->schema) for now!

@MichaelHatherly
Copy link
Contributor

Ah now I realise you perhaps mean deserializing JSON that has these keywords into automatically generated types that use these AllOf{T,S} etc structs?

Yes, that's my main use case.

I want to be able to define some Julia structs, probably nested several layers, and generate a JSON schema that when you happen to generate a valid JSON object for that schema you can directly call JSON3.read("...", MyStruct) on it.

@matthijscox
Copy link
Owner

I agree the full StructTypes integration would be optimal. Or at least make sure we don't block that path forward for a next PR (does it right now?).

I'd also love to see maybe an example string of such a schema. I always forget how JSON schemas actually work :) Nice to add to the readme maybe too if it's not too large.

And given the growing readme we should consider making proper docs. But I can make that a separate issue of it's too much work.

@NickSale
Copy link
Author

NickSale commented May 13, 2025

I want to be able to define some Julia structs, probably nested several layers, and generate a JSON schema that when you happen to generate a valid JSON object for that schema you can directly call JSON3.read("...", MyStruct) on it.

Ah well that's indeed possible here! That's what we're doing with it.

I've made an update to enforce adding any extra 'keyword' properties of structs to StructTypes.excludes so they definitely don't get in the way of deserializing.

What I thought you meant, and is not possible (yet, and I'm not sure is really a good idea) is to generate the types themselves automatically in the sense of https://quinnj.github.io/JSON3.jl/stable/#Generated-Types.

I'd also love to see maybe an example string of such a schema. I always forget how JSON schemas actually work :) Nice to add to the readme maybe too if it's not too large.

Good idea, will do that tomorrow!

@NickSale
Copy link
Author

Added the JSON string to the readme!

One more note on (de)serializing: the AllOf etc structs don't get in the way, but using Val to get const values in the schema definitely does not work with JSON3. They should only be used for these kinds of validation purposes.

@matthijscox
Copy link
Owner

matthijscox commented May 14, 2025

One more note on (de)serializing: the AllOf etc structs don't get in the way, but using Val to get const values in the schema definitely does not work with JSON3. They should only be used for these kinds of validation purposes.

Maybe constant values with Val could be handled by defining your own StructTypes.construct somehow?
I don't know, it feels like a niche use case to me? Why put constant values in a JSON?

Given this issue, I would prefer to separate the Val types from the AllOf types in the readme if possible, to avoid confusing the user. And perhaps mention the fact that Val types cannot directly be (de)serialized by JSON3.

Also please version bump to v0.3.0, then I can directly register after this PR is merged.

@matthijscox
Copy link
Owner

Maybe constant values with Val could be handled by defining your own StructTypes.construct somehow? I don't know, it feels like a niche use case to me? Why put constant values in a JSON?

Given this issue, I would prefer to separate the Val types from the AllOf types in the readme if possible, to avoid confusing the user. And perhaps mention the fact that Val types cannot directly be (de)serialized by JSON3.

Edit: wait your example works fine right? The Val types are in the excluded field. I guess we can leave the example as it is. If people put Val types in non-excluded fields that wouldn't work, but is a weird corner case.

But do please version bump :)

@NickSale
Copy link
Author

NickSale commented May 14, 2025

Indeed, have added a quick note to the readme that this usage of Val should only really be used for validation purposes.

Maybe constant values with Val could be handled by defining your own StructTypes.construct somehow? I don't know, it feels like a niche use case to me? Why put constant values in a JSON?

For reference, the use case I have is that we have a struct containing some feature toggle alongside settings for that feature. These settings should be optional if the toggle is false, but become required if the toggle is true. This can be achieved using anyOf [ schema where feature toggle is off (using toggle : { const : false } ) , schema where feature settings are required ]. (Refactoring could also achieve this without needing anyOf, but unfortunately the flat format where the toggle and settings sit at the same level is fairly set in stone already!)

Also please version bump to v0.3.0, then I can directly register after this PR is merged.

Done!

@matthijscox
Copy link
Owner

matthijscox commented May 15, 2025

AnyOf still feels like a Julia Union to me somehow. Which makes me wonder if we could have nicer syntax for these schema options. But I'm going to have to think about that, so let's just go ahead with this design for now.

Also I'm still wondering if we should make this a trait/function instead of a field? Have you considered that?

So instead of this:

struct BooleanCombinationSchema
    int::Int
    bool::Bool
    allOf::JSG.AllOf{
        JSG.AnyOf{ConstantInt1Schema, ConstantInt2Schema},
        JSG.Not{ConstantBoolTrueSchema}
    }
end
StructTypes.StructType(::Type{BooleanCombinationSchema}) = StructTypes.Struct()
StructTypes.excludes(::Type{BooleanCombinationSchema}) = (:allOf,) 

What if we did this?

struct BooleanCombinationSchema
    int::Int
    bool::Bool
end
StructTypes.StructType(::Type{BooleanCombinationSchema}) = StructTypes.Struct()
    
function JSG.combination(::Type{BooleanCombinationSchema}) = 
  return JSG.AllOf{
          JSG.AnyOf{ConstantInt1Schema, ConstantInt2Schema},
          JSG.Not{ConstantBoolTrueSchema}
      }
end

Now it's clear this is never ever a field that will get (de)serialized by JSON3 struct types, it's a schema thing, like the "requires" field which is also not part of the struct.

@NickSale
Copy link
Author

Oh I like that! I'll give it a try and see how it goes

I did it with fields so the julia struct more closely matches the JSON schema you get out, but it introduces the fiddly need to add to excludes and potentially define a new constructor to hide the keyword fields away.

@NickSale
Copy link
Author

Much nicer for the user and also made the implementation cleaner, good idea!

Could you please rerun the github tests, thanks

@matthijscox
Copy link
Owner

Yeah I like it the more I think about it. This way people can also generate their schema from existing structs if they want, without importing JSONSchemaGenerator into their code and editing those structs.

@NickSale
Copy link
Author

NickSale commented May 15, 2025

AnyOf still feels like a Julia Union to me somehow. Which makes me wonder if we could have nicer syntax for these schema options. But I'm going to have to think about that, so let's just go ahead with this design for now.

In principle we only need to specify the types we want to combine and the fact that we want the anyOf keyword. In fact, now I think about it, this trait-based approach also lets us solve the problem of providing an arbitrary number of schemas to combine (instead of the current 2) by instead providing an object that has an array of types to combine.

Could even do something very generic to provide keywords along the lines of

struct ArrayKeyword   # for making things like anyOf : []
 string keyword
 Type[] subschemas
end
struct SingletonKeyword   # for making things like not : {}, or maybe even const : {}
 string keyword
 Type subschema
end
JSG.keywords(::Type)::Union{SingletonKeyword, ArrayKeyword}[]

But for now I think the specific AnyOf, etc types make it clearer how to use them and more easily avoid incorrect schemas, but I will update these to hold the types to combine as fields rather than parameters. -> this make the logic quite messy

struct Not{T} end

# add use of the above keywords to a struct by specifying a list of them to use with combinationkeywords
combinationkeywords(::Type) = []
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make the default a tuple (), it's generally more optimal than using an array (can be compiled away I think, while arrays cannot)

Maybe we should also add a docstring and export this function, since it's now a formal interface.
The same holds for AllOf, AnyOf, OneOf and Not (although Not may conflict with InvertedIndices.Not)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good one, what do you think of calling them JSONNot, JSONAllOf, etc?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, no that feels ugly :)

We can also choose just not to export for now, or accept a possible clash.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually StructTypes.jl also doesn't export anything, but it does document all the 'public' types and functions

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can add some docstrings then

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also choose just not to export for now

I think this is fine. Then using these types and functions helps make it clear what they are doing, i.e. something to do with schema generation

@NickSale
Copy link
Author

Looking into Tuples made me realise that we can implement validation against enums as well as consts. Have updated the example and tests.

The point here that differentiates this use from a properly defined (and serializable) enum is that we can include things like numbers or reserved keywords like true/false.

@matthijscox
Copy link
Owner

Well everything looks good to me, let's merge and I'll register afterwards.

@matthijscox matthijscox merged commit 308fb07 into matthijscox:main May 16, 2025
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy