Skip to content

Add support for JSON Schema If-Then-Else via :if#1288

Open
esuomi wants to merge 1 commit into
metosin:masterfrom
esuomi:json_schema_ifthenelse_support
Open

Add support for JSON Schema If-Then-Else via :if#1288
esuomi wants to merge 1 commit into
metosin:masterfrom
esuomi:json_schema_ifthenelse_support

Conversation

@esuomi

@esuomi esuomi commented May 10, 2026

Copy link
Copy Markdown

Adds support for JSON Schema's conditional If-Then-Else structure by implementing a new schema type :if to enable detection and handling of the logic when transforming given data to JSON Schema.

This new :if schema type also has equivalence using Malli's existing core schemas (:and/:or/:not) which is internally used for transform and generator support.

Fixes #1287

Adds support for JSON Schema's conditional If-Then-Else structure by
implementing a new schema type :if to enable detection and handling of
the logic when transforming given data to JSON Schema.

This new :if schema type also has equivalence using Malli's existing
core schemas (:and/:or/:not) which is internally used for transform
and generator support.
Comment thread src/malli/core.cljc
(then-v value)
(if else-v (else-v value) true)))))
(-explainer [this path]
(let [then-explainer (-explainer then' (conj path 'then))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The -explainer seems to do extra work. It's enough to simply extend the path here instead of then associng :branch later. And I suggest using keywords as the path elements and implementing LensSchema.

Comment thread src/malli/core.cljc
(-options [this] options)
(-children [this] children)
(-parent [this] parent)
(-form [this] form))))))

@frenchy64 frenchy64 May 12, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Here's the correct implementation here for ParserInfo.

ParserInfo
(-parser-info [_ opts]
  {:simple-parser (and (:simple-parser (-parser-info then' opts))
                       (if else' (:simple-parser (-parser-info else' opts))  true))})

There are some other useful methods missed that are implemented in the surrounding schemas.

EDIT: actually this depends on which implementation of -parser we use so let's defer this discussion until that is resolved.

Comment thread src/malli/core.cljc
(let [then-u (-unparser then')
else-u (when else' (-unparser else'))]
(fn [x]
(if (condition-v x)

@frenchy64 frenchy64 May 12, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't think this is right. x here is potentially parsed so it's meaningless to test the condition validator. For example, consider [:if :int [:orn [:int :int]]]. It's now a tag at this point, so will always fail the :int validator.

I think there's a few options to choose here:

  1. we return a tagged result from -parser similar to :orn
  2. we ensure :if never transforms the parsed value by using -simple-parser
  3. we try each unparser in turn similar to :or

Comment thread src/malli/core.cljc
(-transformer [this transformer method options]
(-transformer equivalent-schema' transformer method options))
(-walk [this walker path options]
(-walk-leaf this walker path options))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

:if is not a leaf.

Comment thread src/malli/core.cljc
[:map [:y string?]]]] ; ELSE
```

The `ELSE` branch can be omitted for `when`-like behavior."

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It's more like logical implication as written. This is the opposite of what I expected. For example [:or [:if S T] [:if S' T']] would never hit the second schema.

Additionally, the equivalent schema to [:if S T] is actually [:or [:and S T] :any], so there's some internal inconsistency here.

This might need more time to bake, I propose we support 2 args later and start with 3. I understand JSON Schema maps missing branches to true but this doesn't map nicely to Clojure's semantics.

@opqdonut opqdonut left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ambrose reviewed the impl so I just had a look at the tests & docs.

:says "meow"}
decoded {:animal :cat
:says :meow}]
(testing "roundtip works"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[typo] roundtRip

decoded {:animal :cat
:says :meow}]
(testing "roundtip works"
(is (= encoded (m/encode schema decoded (mt/string-transformer))))))) No newline at end of file

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I was expecting this to also test that (= decoded (m/decode ...))

["card-number"]}
:else {:properties {"iban" {:type "string"}}
:required ["iban"]}}
(json-schema/transform if-then-else-schema)))))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

tests should use transform-and-check, which uses AJV to validate the schema

Comment thread test/malli/core_test.cljc
(is (m/coerce ConsCell [1 [2 [3 [4 [1 [2 [3 [4 nil]]]]]]]]))
(is (= @count-into-schemas 3)))) ;; was 10

(deftest if-conditional-schema-test

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

good test, though it doesn't exercise the 2-arity form at all!

result (json-schema/transform type-emitting-schema)]
(is (= "object" (get-in result [:if :type])))
(is (= "object" (get-in result [:then :type])))
(is (= "object" (get-in result [:else :type])))))) No newline at end of file

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd like this test to show what happens when something other than :map is used inside :if

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

JSON Schema If-Then-Else conditional construct support

3 participants