cue icon indicating copy to clipboard operation
cue copied to clipboard

evaluator: encoding of JSON Schema (and other) oneofs

Open myitcv opened this issue 1 year ago • 3 comments

What version of CUE are you using (cue version)?

$ cue version
cue version v0.0.0-20240522083645-3ce48c0beadf

go version go1.22.3
      -buildmode exe
       -compiler gc
  DefaultGODEBUG httplaxcontentlength=1,httpmuxgo121=1,tls10server=1,tlsrsakex=1,tlsunsafeekm=1
     CGO_ENABLED 1
          GOARCH arm64
            GOOS linux
             vcs git
    vcs.revision 3ce48c0beadfc8b3d22285a197c9ebd53f4bd59a
        vcs.time 2024-05-22T08:36:45Z
    vcs.modified false
cue.lang.version v0.9.0

Does this issue reproduce with the latest release?

Yes

What did you do?

Capturing an issue that deals with how to encode one-ofs from JSON Schema, protocol buffers (and potentially other languages). #943 suggests one approach for encoding oneofs; this issue is a more general exploration of the space.

The example in question is from JSON Schema:

{
  "oneOf": [
    {
      "required": [
        "x"
      ]
    },
    {
      "required": [
        "y"
      ]
    }
  ]
}

As can be seen from https://jsonschema.dev/s/5KO7a, this validates successfully when one field is present, and raises an error when both are specified https://jsonschema.dev/s/209Bo.

Turning to how this can be represented in CUE we have, AFAICT, two ways of doing this today:

// approach 1
_constraint: {x!: int} | {y!: int}
// approach 2
_constraint: {x!: int, y?: _|_} | {y!: int, x?: _|_}

Looking at how each behaves with both the old and new evaluator we see the following:

# Old evaluator - approach 1
env CUE_EXPERIMENT=''
! exec cue export x.cue approach1.cue
cmp stderr old_1_stderr.golden

# Old evaluator - approach 2
env CUE_EXPERIMENT=''
exec cue export x.cue approach2.cue
cmp stdout old_2_stdout.golden

# New evaluator - approach 1
env CUE_EXPERIMENT='evalv3'
! exec cue export x.cue approach1.cue
cmp stderr new_1_stderr.golden

# Old evaluator - approach 2
env CUE_EXPERIMENT='evalv3'
exec cue export x.cue approach2.cue
cmp stdout new_2_stdout.golden

-- approach1.cue --
package x

_constraint: {x!: int} | {y!: int}
-- approach2.cue --
package x

_constraint: {x!: int, y?: _|_} | {y!: int, x?: _|_}
-- x.cue --
package x

res: _constraint & {
	x: 5
}
-- old_1_stderr.golden --
res: incomplete value {x:5} | {x:5,y!:int}
-- new_1_stderr.golden --
res: incomplete value {x:5} | {x:5,y!:int}
-- old_2_stdout.golden --
{
    "res": {
        "x": 5
    }
}
-- new_2_stdout.golden --
{
    "res": {
        "x": 5
    }
}

What did you expect to see?

Unclear.

Export of approach 2 succeeds, but I don't think it is a scaleable approach to encoding oneofs. It is also not readily understandable by humans. It's not incorrect however that the cue export succeeds in this case.

Export of approach 1 fails. Given my understanding of when the required field is "checked", I think this therefore qualifies as "behaving as expected". But I'm not clear that this is the behaviour we want. I think it's reasonable to make the argument that cue export explicitly/implicitly says "I am not going to provide you with anything more". Through that lens, one could argue that the {y!: int} disjunct in approach 1 can be eliminated, because it fails with respect to {x: 5}, which would then allow the export to succeed.

Hence there's an argument that the test should fail, because (if you will excuse the double negative) approach 1 should succeed and give the same output as approach 2.

What did you see instead?

Passing test.

Related issues

In raising this issue I suggest we consolidate conversation from the following issues:

Update: those issues have now been closed to consolidate discussion here.

myitcv avatar May 22 '24 13:05 myitcv

@rogpeppe also flags this canonical examples from JSON Schema: https://json-schema.org/understanding-json-schema/reference/combining#oneOf

{
  "oneOf": [
    { "type": "number", "multipleOf": 5 },
    { "type": "number", "multipleOf": 3 }
  ]
}

myitcv avatar Jul 22 '24 09:07 myitcv

Adding one more example We are facing troubles with correct code generation because oneOf has allOf + not anyOf

import "strings"

#SpecialText: {
  type: "special",
  text: string & =~"^.{1,}$"
}

#MyText: {
  content: strings.MinRunes(1) | #SpecialText
}
"components": {
        "schemas": {
            "MyText": {
                "type": "object",
                "required": [
                    "content"
                ],
                "properties": {
                    "content": {
                        "oneOf": [
                            {
                                "allOf": [
                                    {
                                        "type": "string",
                                        "minLength": 1
                                    },
                                    {
                                        "not": {
                                            "anyOf": [
                                                {
                                                    "$ref": "#/components/schemas/SpecialText"
                                                }
                                            ]
                                        }
                                    }
                                ]
                            },
                            {
                                "$ref": "#/components/schemas/SpecialText"
                            }
                        ]
                    }
                }
            },
            "SpecialText": {
                "type": "object",
                "required": [
                    "type",
                    "text"
                ],
                "properties": {
                    "type": {
                        "type": "string",
                        "enum": [
                            "special"
                        ]
                    },
                    "text": {
                        "type": "string",
                        "pattern": "^.{1,}$"
                    }
                }
            }
        }
    }

chiragjn avatar Aug 13 '24 08:08 chiragjn

I've just raised https://github.com/cue-lang/cue/issues/3380 to track the implementation of oneof and related constraints using the new matchN builtin.

myitcv avatar Aug 19 '24 12:08 myitcv

What remains to be done here? https://github.com/cue-lang/cue/issues/3380 tracked proper support for oneOf in jsonschema and it was resolved. The related commits in master mentioned this issue, but did not mark it as closed. It's not clear to me what work remains to do in this space, if any.

mvdan avatar Dec 26 '24 16:12 mvdan

I'll let @rogpeppe opine.

myitcv avatar Dec 31 '24 06:12 myitcv

We can close this on the basis that oneof support in JSON Schema is done, and the language has support for them in general in the form of matchN. If other encodings struggle with encoding or decoding oneofs in the future, we can create separate issues to track those.

mvdan avatar Jan 08 '25 15:01 mvdan