Skip to content
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

Should unevaluatedProperties count annotations from if? #1093

Closed
jdesrosiers opened this issue Apr 22, 2021 · 26 comments
Closed

Should unevaluatedProperties count annotations from if? #1093

jdesrosiers opened this issue Apr 22, 2021 · 26 comments

Comments

@jdesrosiers
Copy link
Member

The current interpretation is that if does count annotations if it passes. We have tests that check for this and every current implementation behaves that way. However, @Relequestual brought up on slack that annotations should never count for if because it's just a conditional whose purpose is to select between then and else.

Here's an example that illustrates the consequences of making such a change.

{
  "type": "object",
  "if": {
    "properties": {
      "foo": { "const": "then" }
    },
    "required": ["foo"]
  },
  "then": {
    "properties": {
      "bar": { "type": "string" }
    },
    "required": ["bar"]
  },
  "unevaluatedProperties": false
}

If we don't count if annotations, then there is no value that matches the if and passes validation over the whole schema. { "foo": "then", "bar": "" } would pass the if/then, but fail the unevaluatedProperties. The "foo" property declaration would have to be added outside of the if. If we can determine any reason why that would be a bad thing, then we shouldn't change anything. I can't think of any good reason for it.

Another way to think about it is, what is the behavior of unevaluatedProperties using the Implication Pattern. if/then is supposed to be sugar for the Implication Pattern, so I would expect them to work the same with unevaluatedProperties. Here's the same example as above except with implication,

{
  "type": "object",
  "anyOf": [
    {
      "not": {
        "properties": {
          "foo": { "const": "then" }
        },
        "required": ["foo"]
      }
    },
    {
      "properties": {
        "bar": { "type": "string" }
      },
      "required": ["bar"]
    }
  ],
  "unevaluatedProperties": false
}

Now the annotations from the "if" don't apply because of the not schema. So, that indicates that it might be desirable for if to not count annotations to be compatible with implication. To me, that's a pretty convincing argument that we go this wrong and annotations from if schemas should never be counted.

@karenetheridge
Copy link
Member

karenetheridge commented Apr 23, 2021

I find this a compelling argument. +1 from me.

edit: I am changing my vote to 0, in the face of compelling arguments from the other side. :)

@notEthan
Copy link
Contributor

I also wondered why this is the specified behavior, and had intended to open an issue about it. it is counterintuitive to me that annotations from if - either validation annotations, or informational, would apply. +1 from me for changing to not collect these annotations.

@handrews
Copy link
Contributor

I absolutely intended for annotations to be collected from if whenever it passes. Sometimes I don't remember what I meant, but this time I do 😄

The validation result from if is essentially discarded after determining whether to evaluate then or else, but annotations are explicitly said to be collected:

If annotations (Section 7.7) are being collected, they are collected
from this keyword's subschema in the usual way, including when the
keyword is present without either "then" or "else".

When I wrote "in the usual way" I meant collecting them and propagating them up as normal if the if schema passes, and dropping all annotations from the if schema if it fails. That could perhaps be spelled out more clearly, but that is definitely what I meant. The unevaluatedProperties case is one reason for that, as otherwise you have to duplicate some part of the if schema in the then schema in order to make it work, which is not good design.

including when the keyword is present without either "then" or "else" means that you can use a standalone if to conditionally collect some annotations without ever causing validation to fail. I don't think I had a use case in mind, but it felt more consistent, and earlier drafts failed to explain what a standalone if meant (other than that someone forgot a then or else, most likely).

@gregsdennis
Copy link
Member

So are you saying that then runs when if produces annotations, and else runs when if does not produce annotations but is still present?

@Relequestual
Copy link
Member

So are you saying that then runs when if produces annotations, and else runs when if does not produce annotations but is still present?

That seems to be the gist of it. Why do you ask?

@Relequestual
Copy link
Member

I absolutely intended for annotations to be collected from if whenever it passes. Sometimes I don't remember what I meant, but this time I do 😄

Do you remember what the use case you had in mind for this was @handrews ?

The validation result from if is essentially discarded after determining whether to evaluate then or else, but annotations are explicitly said to be collected:

If annotations (Section 7.7) are being collected, they are collected
from this keyword's subschema in the usual way, including when the
keyword is present without either "then" or "else".

When I wrote "in the usual way" I meant collecting them and propagating them up as normal if the if schema passes, and dropping all annotations from the if schema if it fails. That could perhaps be spelled out more clearly, but that is definitely what I meant.

Yeah, that's how I read the spec, and why I asked for the issue to be raised. I certinly didn't expect this to be the case, and I'd have raised a concern if I'd have understood the implications at the time.

The unevaluatedProperties case is one reason for that, as otherwise you have to duplicate some part of the if schema in the then schema in order to make it work, which is not good design.

I actually think this is preferable design, which I often suggest, but putting minimal content in then.
I always suggest people break up their validation into the conditional logic and remaining subschema application logic. I find it's cleaner, and allows for better decoupling of concerns. Plus, useful if you want to later break up your schema into multiple files or refactor.

including when the keyword is present without either "then" or "else" means that you can use a standalone if to conditionally collect some annotations without ever causing validation to fail. I don't think I had a use case in mind, but it felt more consistent, and earlier drafts failed to explain what a standalone if meant (other than that someone forgot a then or else, most likely).

I truly expected if on its own to have zero implications, and that it could be ignored.


However, having said all that, I can't think of an example of where this (as is currently) would be a problem, and I can think of potential use cases in terms of annotation collections beyond just for unevaluated* keywords.

This leaves me feeling a little conflicted.

@Relequestual
Copy link
Member

The unevaluatedProperties case is one reason for that, as otherwise you have to duplicate some part of the if schema in the then schema in order to make it work, which is not good design.

I actually think this is preferable design, which I often suggest, but putting minimal content in then.
I always suggest people break up their validation into the conditional logic and remaining subschema application logic. I find it's cleaner, and allows for better decoupling of concerns. Plus, useful if you want to later break up your schema into multiple files or refactor.

Taking @jdesrosiers implication pattern example from the first post. If you remove the unevaluatedProperties, using anyOf doesn't make sense to me here. I would probably suggest something like this was written as...

[some time passes]

It appears I can't build schemas today...
I do normally find the implication pattern unintiutive, but I my gut tells me I'd have arrived at a different solution where the annotations would have come out as expected. (Given I have no evidence, it's not a pragmatic argument at all 😩)

@gregsdennis
Copy link
Member

That seems to be the gist of it. Why do you ask?

It seems fine to me that then should respond to an annotation - the annotation implies the presence of if. This is how "keyword communication" seems to work.

Annotations are the mechanism by which keywords can know that other keywords are present. If no annotation is present, it should be equivalent to the keyword that generates it being absent.

But that's not the case for else. else shouldn't run if if isn't present (so no annotations), but it should run if if is present but still doesn't produce annotations.

In other words, else seems to be an oddball keyword that actually scans its own schema for the presence of another keyword. All of the other keyword interdependencies can be managed in the second keyword by whether annotations are produced by the first.

@jdesrosiers
Copy link
Member Author

when the keyword is present without either "then" or "else" means that you can use a standalone if to conditionally collect some annotations without ever causing validation to fail.

That feels like an abuse of the keyword to me, but I could be convinced otherwise with a good example.

I actually think this is preferable design, which I often suggest, but putting minimal content in then. I always suggest people break up their validation into the conditional logic and remaining subschema application logic. I find it's cleaner, and allows for better decoupling of concerns. Plus, useful if you want to later break up your schema into multiple files or refactor.

💯 Completely agree

@karenetheridge
Copy link
Member

In other words, else seems to be an oddball keyword that actually scans its own schema for the presence of another keyword.

That's not unique. maxContains and minContains are also dependent on the presence of another keyword (contains), and if it is not present, these keywords do nothing

@karenetheridge
Copy link
Member

That feels like an abuse of the keyword to me, but I could be convinced otherwise with a good example.

Abuse is a bit negative.. how about "(sometimes) convenient side effect". I view the "not": { "not": { ... } } pattern similarly - the facct that it serves to hide annotations from the inner schema is useful, but the keyword wasn't designed for this purpose specifically. I'm sure we can all think of other similar side effects in our primary languages that are (ab)used as a pattern. There was a famous stackoverflow post on "the --> operator) (as in while (i --> 0) { // while i decreases to zero..) a while back which is another one of these cases.

@handrews
Copy link
Contributor

handrews commented May 1, 2021

Ooof. I am shocked to discover that section 7.2 reads:

Keyword behavior MAY be defined in terms of the annotation results of
subschemas (Section 4.3.5) and/or adjacent keywords (keywords within
the same schema object) and their subschemas. Such keywords MUST NOT
result in a circular dependency. Keywords MAY modify their behavior
based on the presence or absence of another keyword in the same
schema object (Section 4.3).

instead of (note the boldface addition):

Keyword behavior MAY be defined in terms of the annotation or assertion results of
subschemas (Section 4.3.5) and/or adjacent keywords (keywords within
the same schema object) and their subschemas. Such keywords MUST NOT
result in a circular dependency. Keywords MAY modify their behavior
based on the presence or absence of another keyword in the same
schema object (Section 4.3).

The whole point of this section is that you can define an execution order for certain keywords with respect to other keywords or other keyword classes (e.g. in-place applicators), and affect the later-executing keywords with the results of the earlier-executing keywords. While this is most notably done with annotation results, it was supposed to allow using either or both types of results.

  • if runs before then or else
  • prefixItems runs before items, which runs before unevaluatedItems
  • properties and patternProperties run before additionalProperties, which runs before unevaluatedProperties
  • in-place applicators run before unevaluated*

I might be missing something but you get the point.

I thought I had a very clear memory of writing that paragraph with both kinds of results, but I guess I only thought about it? It was something I figured out after first writing the part about annotations, so it's not too shocking to find that I never actually fixed it, I suppose.

However, the specification for if is very clear that the selection of then vs else is done based on the assertion result and not an annotation:

This validation outcome of this keyword's subschema has no direct
effect on the overall validation result. Rather, it controls which
of the "then" or "else" keywords are evaluated.

"validation result" here is synonymous with "assertion result," I'd always meant to go back and replace all of the "validation result"s with "assertion result"s as it is more generic, but apparently I never did.

I consider this a simple error in section 7.2. The phrase or assertion was intended to be in that sentence and got left out by error. I'm tempted to file it as errata on the draft, that's the degree to which I consider this to just be a bug/typo.

To sum up:

  • Applying then vs else is done based on the assertion result of if
  • If if is not present, then neither then nor else are ever applied
  • if is always applied, and in terms of annotation results it is a totally normal subschema application

All of this is necessary for it (including interaction with unevaluated* to work at all. If you take any of it out the whole thing falls apart. Forbidding if on its own is more trouble than it's worth. It's weird to use it on its own and I don't know that I'd recommend it, but there are a lot of weird not-recommended things you can do in JSON Schema. This one isn't a problem, it's just a bit odd.


@Relequestual

Do you remember what the use case you had in mind for this was @handrews ?

Yes, the unevaluted* keywords. They only work if annotations are collected. I really thought the annotation collection behavior was quite clear. I made a point of calling it out specifically, just as I specifically called out that the assertion result was to be discarded after determining whether to apply then or else.

@handrews
Copy link
Contributor

handrews commented May 1, 2021

(and yes, I missed the contains keywords but @karenetheridge already noted those- thanks!)

@handrews
Copy link
Contributor

handrews commented May 2, 2021

@Relequestual

The unevaluatedProperties case is one reason for that, as otherwise you have to duplicate some part of the if schema in the then schema in order to make it work, which is not good design.

I actually think this is preferable design, which I often suggest, but putting minimal content in then.
I always suggest people break up their validation into the conditional logic and remaining subschema application logic. I find it's cleaner, and allows for better decoupling of concerns. Plus, useful if you want to later break up your schema into multiple files or refactor.

This doesn't make sense to me. If your condition is a test against property "foo", and your remaining logic has to do with property "bar", then schema authors "break[ing] up their validation into the conditional logic and remaining subschema application logic" would exactly mean putting the subschema for "foo" under if, and the subschema for "bar" under then, and not mentioning "bar" in if or "foo" in then.

I can definitely see an abstract argument that the entire if schema should have no results, but I think the concrete implications of doing so- that successfully evaluating a property under if would fail to affect unevaluatedProperties- are too problematic to ignore.

Part of the whole point of unevaluatedProperties was to make it so that you no longer had to keep putting "someProperty": true in odd places to make additionalProperties work.

@handrews
Copy link
Contributor

handrews commented May 2, 2021

@jdesrosiers

when the keyword is present without either "then" or "else" means that you can use a standalone if to conditionally collect some annotations without ever causing validation to fail.

That feels like an abuse of the keyword to me, but I could be convinced otherwise with a good example.

I get where that is coming from, but if you think of a standalone if as one that implicitly has a "then": true, "else": true attached to it, it's weird, but not really abusive. I don't actually remember why then and else don't default to true, although I do remember that trying to set defaults for if/then/else was more complicated than expected. But it also took us a couple of tries to nail down how it really worked in general, so maybe defaults would make sense now?

@jdesrosiers
Copy link
Member Author

All I meant by the "abuse of the keyword" comment was that if that's the only reason we would want if annotations to be collected, it's not a good reason because it's not how the keyword was intended to be used or can be usefully used.

All of this is necessary for it (including interaction with unevaluated* to work at all. If you take any of it out the whole thing falls apart.

I'm not following why not collecting if annotations would break unevaluated*. It's probably because I don't implement unevaluated* using annotations the way the spec suggests. If this would break unevaluated*, then that's definitely a problem. It would be good to hear from implementers that implemented unevaluated* in the recommended way whether not collecting if annotations would be problematic for them.

@gregsdennis
Copy link
Member

One of the big changes moving from Manatee.Json and writing json-everything was actually processing annotations. I made a lot of discoveries in doing so.

My new implementation absolutely breaks unevaluated* if if doesn't produce annotations.

@handrews
Copy link
Contributor

handrews commented May 2, 2021

@jdesrosiers in an annotation-based implementation, this schema (I'm including "else": true to make a comparison more clear further down):

{
    "if": {
        "required": ["foo"],
        "properties": {"foo": {"const": true}}
    },
    "then": {
        "required": ["bar"],
        "properties": {"bar": {"type": "string"}}
    },
    "else": true,
    "unevaluatedProperties": false
}

This instance should pass: {"foo": true, "bar": "whatever"}

If if does not collect the annotation necessary for unevaluatedProperties to see the "foo" property as having been successfully evaluated, then the instance will fail because "foo" and "bar" are both present, but only "bar" will be annotated as having been evaluated, so the presence of "foo" will cause unevaluatedProperties to fail.

Note that {"foo": false} will fail, which might seem a bit counter-intuitive. But to write out the schema another way:

{
    "oneOf": [
         {
            "allOf": [
                {
                    "required": ["foo"],
                    "properties": {"foo": {"const": true}}
                },
                {
                    "required": ["bar"],
                    "properties": {"bar": {"type": "string"}}
                }
            ]
        },
        {
            "allOf": [
                {
                    "not": {
                        "required": ["foo"],
                        "properties": {"foo": {"const": true}}
                    }
                },
                true
            ]
        }
    ],
    "unevaluatedProperties": false
}

Does this make it clear? These schemas are only equivalent with the behavior I intended to specify.

The allOf in the first branch of the oneOf collects the annotation noting the evaluation of "foo" in its first subschema- that is the equivalent of collecting from if. It collects one for "bar" in the second subschema - the equivalent of then. For {"foo": true, "bar": "whatever"}, it passes this first branch of the oneOf, fails the second (so passes overall), and then the unevaluatedProperties sees that there are no unevaluated properties and the whole thing passes.

For {"foo": false} we fail the first branch of the oneOf (so no annotations) and pass the second branch, passing overall. But the equivalent of the if in the second branch is the negated form, and not always drops annotations whether the subschema passes or fails. The second entry in the second branch is the true corresponding to "else": true. Boolean schemas never collect annotations, so we pass the oneOf with no annotations from it at all, unevaluatedProperties sees foo, and causes it all to fail because foo has not been successfully evaluated.

@jdesrosiers
Copy link
Member Author

@handrews Thanks for taking the time to write up that example, but I'm not sure it answers my question. That explains how it works, but I don't see how it explains why it can't work without if annotation collection. In case it's not clear, I do expect that not collecting annotations would change validation behavior. I'm not expecting that you can stop collecting annotations and still pass all the current tests.

@handrews
Copy link
Contributor

handrews commented May 3, 2021

@jdesrosiers

In case it's not clear, I do expect that not collecting annotations would change validation behavior. I'm not expecting that you can stop collecting annotations and still pass all the current tests.

OK how in the heck do you both change the validation behavior and say that it still "works"? What definition of "works" are you using here? I'm extremely confused at this point. The correct definition of "works" is what I wrote in the spec. The reason that that is correct is not because I'm the one who wrote it, but because if/then/else was explicitly accepted for the specific reason that it was purely syntactic sugar equivalent to combinations of allOf/oneOf/not as shown above.

That is how the keyword was intended to work, and any other outcome is wrong.

@jdesrosiers
Copy link
Member Author

@handrews I don't think there is any argument about what the spec says and how it currently works. The issue was about whether or not the spec should be changed. What I'm attempting to facilitate conversation around is whether there is any reason it couldn't or shouldn't be changed. Would changing it make things harder for schema authors? Would schema authors not be able to express something they couldn't before? Does it fit into the JSON Schema architecture or do implementers need to hack something to make it fit? The change would "work" if it doesn't remove the ability to express any assertions you could before and it can be implemented in a reasonable way.

I think the assertions you are able to express and the ease of implementation are the same which ever way it is defined. If I'm right about that, the only questions left to answer are (1) which equal alternative do we prefer, and (2) is it worth making a breaking change for something that isn't broken even if we decide it's not what we prefer?

(I'm aware that if/then/else is sugar. See the second half of the original post.)

@handrews
Copy link
Contributor

handrews commented May 3, 2021

@jdesrosiers

The issue was about whether or not the spec should be changed.

OK, I had thought it was more a question about what the spec says for some reason. I take it you mean this question:

The "foo" property declaration would have to be added outside of the if. If we can determine any reason why that would be a bad thing, then we shouldn't change anything. I can't think of any good reason for it.

And I have said repeatedly that having to duplicate the evaluation of a property that is evaluated in the if is anti-DRY and unequivocally bad. That holds true whether the duplication has to be in the then, the else, or outside of the if entirely.

Related to that objection of mine... @Relequestual I might have just realized why your comment confused me. Regarding this statement:

I always suggest people break up their validation into the conditional logic and remaining subschema application logic.

as a reason for if to not return annotations: I think the key word is "remaining." I interpreted that to mean:

{
    "if": {
        "required": ["foo"],
        "properties": {"foo": {"const": true}}
    },
    "then": {
        "required": ["bar"],
        "properties": {"bar": {"type": "string"}}
    }
}

as in, "foo" was validated under if so the remaining subschema application logic consists solely of the logic for "bar".

But if, by remaining you meant "all logic that has to do with validating, regardless of what logic is used in the conditional," then I guess you would call this the best practice:

{
    "if": {
        "required": ["foo"],
        "properties": {"foo": {"const": true}}
    },
    "then": {
        "required": ["foo", "bar"],
        "properties": {
            "foo": {"const": true},
            "bar": {"type": "string"}
        }
    }
}

as now your then schema is the complete validation. Is that what you meant? If so, what is even the point of the if? I suppose it's a slight optimization... maybe. in some circumstances. But it also feels a lot like writing an if statement in code and then asserting the if condition in the then block. All that is is an opportunity to introduce an error when they get out of sync. Which they eventually will, given long enough.


Ultimately I'm very much against changing this. It works in a way that is consistent with everything else and does what we said we were going to do when we implemented both if/then/else and unevaluatedProperties. The part where the keyword interaction paragraph fails to specify that keywords can depend on each other's validation results as well as annotation results is confusing, but also really just an unfortunate typo (like that time we published a draft with exclusiveMinimum and exclusiveMaximum reversed- it didn't mean that implementations actually reversed the behavior).

@jdesrosiers you gave an example with anyOf, but that example did a non-intuitive conversion between "if thing then stuff" to "any of not-thing or stuff", which is really not the same thing. To address the obvious objection that I'm just asserting what is and is not an acceptable conversion, in the original issue, Evgeny shows the intended translation in two different places (and if/then/else exists because Evgeny implemented in AJV and it was popular there- although of course this predated the concept of annotation collection).

I don't consider the "any of not-thing or stuff" conversion to be correct, so making if behave identically to it would be breaking the keyword to me. And I don't see any other clear argument in favor of the change (aside from perhaps @Relequestual's best practices question, which I addressed above).

I think the assertions you are able to express and the ease of implementation are the same which ever way it is defined.

I'm not convinced, but I definitely dispute that these are the only considerations. It behaves consistently the way it is, and it would not be consistent the other way around. Of course if you're willing to just shove nots/anyOfs/oneOfs/allOfs around arbitrarily until they support your point, you can claim that you can express whatever either way, and if we'd never previously discussed conversions that could be a valid debate. But the if keywords have a well-defined meaning in terms of the not and *Of keywords, and I have not see anything that convinces me we should change its behavior several drafts down the road.

Given the conversion that Evgeny used originally and I used a few comments up, annotation collection has to work the way I specified it.

@jdesrosiers
Copy link
Member Author

having to duplicate the evaluation of a property that is evaluated in the if is anti-DRY and unequivocally bad.

@Relequestual's argument refutes this, but you're absolutely right that this hasn't been properly explained. This is the pattern I try to teach people and I'm pretty sure is very close if not exactly what @Relequestual was talking about.

{
  "type": "object",
  "properties": {
    "foo": { "const": true },
    "bar": { "type": "string" }
  },
  "unevaluatedProperties": false,

  "allOf": [{ "$ref": "#/$defs/if-foo-is-true-then-bar-is-required" }],

  "$defs": {
    "if-foo-is-true-then-bar-is-required":  {
      "if": {
        "properties": {
          "foo": { "const": true }
        },
        "required": ["foo"]
      },
      "then": { "required": ["bar"] }
    }
  }
}

There are three distinct sections. In the first section, all properties and their general constraints are defined. In the second section, any conditionals or other complex constraints are referenced using definitions that give the constraint a clear description. Developers can now read just the first few lines of straightforward schema and have a good understanding of everything this schema describes. They can continue to read if they want to learn more.

If "foo" is only defined in the if, a reader can no longer fully understand the schema just by looking a the first few simple lines. However, the real problem would be error reporting. If "foo" is present and has value other than true, the error you will get with this schema is what you would expect: "foo" can only have the value true. However, if "foo" was only defined in the if, the error you would get is more confusing: "foo" is an unevaluated property. That's why I consider it best practice to define the property separately even if it means duplicating it.

(Not really relevant, but I'll mention it in case it comes up ...) If the intent is that, if foo is not true then "bar" should be unevaluated rather than optional, then you would have to define "bar" in the then which breaks the nice readability we had going in that schema. However, if you insist on locking down schemas with additionalProperties and/or unevaluatedProperties, then there are always going to be some compromises required.


you gave an example with anyOf, but that example did a non-intuitive conversion between "if thing then stuff" to "any of not-thing or stuff", which is really not the same thing.

I can't parse half of that sentence, but I can say for sure that it is the same thing as Evgeny's pattern. I did the math just to be sure.

Of course if you're willing to just shove nots/anyOfs/oneOfs/allOfs around arbitrarily until they support your point, you can claim that you can express whatever either way

This statement is unnecessarily aggressive. Let's keep this civil. Boolean implication is far from "arbitrary". Implication is the standard way to express conditionals in boolean algebra. It's denoted a -> b and pronounced "a" implies "b". It means that if and only if "a" is true, then "b" must also be true. The identity transformation for implication is !a || b (using a notation familiar to programmers). In schema form, that's "anyOf": [{ "not": A}, B] which is what I presented. If we include else, it would be (a -> b) && (!a -> c) which can be proven to be equivalent to Evgeny's (a && b) || (!a && c) (the XOR (oneOf) is not actually necessary, OR (anyOf) is sufficient).


I'm convinced by Evgeny's non-standard expression of a conditional that it can be expressed in a way that doesn't require the if schema to be negated like implication does. Therefore, any argument about how if/then/else is transformed no longer caries any weight for me when determining whether or not if should collect annotations. I'm convinced that nothing is lost by changing if to not collect annotations (see first part of this comment). I think if/then/else can be defined either way and there is little practical difference. Therefore, my position is, if it's not broke, don't fix it. There's no compelling reason to make a backwards incompatible change. There is nothing wrong with the way it's defined now and nothing gained by changing it. It's just different. If we were doing this from scratch I'd prefer that if doesn't collect annotations, but that's not the case.

@handrews
Copy link
Contributor

handrews commented May 5, 2021

@jdesrosiers

Therefore, my position is, if it's not broke, don't fix it. There's no compelling reason to make a backwards incompatible change. There is nothing wrong with the way it's defined now and nothing gained by changing it. It's just different.

OK it sounds like we're in agreement on a course of (non)action, so the main point here is probably resolved (I'll leave this open in case @Relequestual still wants to chime in).


[EDIT: this part came across as disparaging towards @jdesrosiers when it was absolutely not intended to- see comment further down for details and apology.]

Regarding what you would recommend as a best practice for schema organization: that is pretty much the opposite of what I would recommend 😅 And while I do not maintain an implementation, I have worked with very large schema collections used by teams of wildly different technical experience levels, across global time zones, and different industries. So unlike my opinions on implementation factors, my views on what does and doesn't work in schema authorship are rooted in reality.

That definitely does not mean your example is in any way "wrong," but unless we have a consensus as an org on what a best practice really is, using our personal schema design styles to justify features is not supportable. There needs to be more to it than one or even a few of us liking things a certain way.

I'm not going to compare our recommendations because, again, I don't think your recommendation is wrong, and I have no desire to try to win a point here (and I don't think you're trying to do that either).


Finally, on a more important point:

Of course if you're willing to just shove nots/anyOfs/oneOfs/allOfs around arbitrarily until they support your point, you can claim that you can express whatever either way

This statement is unnecessarily aggressive. Let's keep this civil.

Yeah, that's fair. Ironically I had actually reworked it to be less aggressive but either my notion of "less aggressive" was impaired at the time or I accidentally reverted to the earlier sentence. Either way, it shouldn't have been there and I apologize. This is something that I need to do better with.

I also want to bring up another aspect of keeping things civil, which is respecting past work. This is a tricky topic because of course we have changed many things over the years with JSON Schema. I started to examine that in detail in this comment, but I think that conversation belongs somewhere else. I know @Relequestual has been setting up other process sorts of things, so perhaps he has thoughts on where that should go?

@Relequestual
Copy link
Member

What @jdesrosiers shows in the schema in #1093 (comment) is what I'm talking about.

I try to write code, and schemas, for people to read and understand.
Even when dealing with sub 10 schemas, if you can separate logic and model in your schemas, they are far more maintainable, and can be refactored easier should you wish.

There are multiple situations where I feel this approach to writing schemas makes things easier, but I won't go into details here as it's not the place and people still need to buy my book... 😅

Just because I feel that one approach is best practice, it doesn't mean the other approach is invalid or wrong.

I can understand the reasoning behind it being set this way, and we have tests for it. I was just not expecting it.

On balance, I feel the best thing to do here is keep things "as is" and wait to see how the community reacts, and if any problems are identified.

As I opened this issue, I'll close it.


@handrews if you want to open an issue in relation to #1093 (comment), please do! I don't see why it can't, and shouldn't, allow for both, as I believe you're suggesting it should.

@handrews
Copy link
Contributor

handrews commented May 6, 2021

It's been brought to my attention that the way I described how I would handle the scenario being discussed appears to disparage what @jdesrosiers wrote as a solution. And further, that my citing of my experience reads as implying that he lacks such experience (which is not true) as a way to invalidate his argument. I apologize for all of that- I should have been much more careful about acknowledging the validity and authority of his proposal. My intent was simply to point out that there are two valid, real-world ways to approach that problem, and therefore neither approach proves much of anything since both are valid.

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

No branches or pull requests

6 participants