In another much more generic customer facing API I instead used a custom schema, as I needed to do other things that would not have been possible in it like embedding the API documentation in the schema so the front-end could display help on-the-fly and use the schema to dynamically generate forms. The advantage was also that being fully in control of the syntax I could create ways to express constraints like field X is valid only in searches, field Y is valid only in PUT/PATCH but not POST, etc. etc.
This said especially for straightforward APIs I feel JSON schemas are quite useful, being able to have a single source of truth on what your messages look like that you can share with front-end, back-end and customers is quite powerful. One thing is though that validation errors are not very user-friendly unfortunately. This was all around draft-04 time IIRC.
The only thing that is counter-productive is the different schema iterations; at the time v3 and v4 have been different in some key areas. Haven't looked into v7 yet, but I hope its not "breaking as much" as v3 vs v4 did.
1. if/then/else which was added in Draft-07.
2. Custom errors. For Node, I use ajv and ajv-errors.
With the combination of if/then/else and custom errors, I have found that I can always return a single, useful error to the API user regardless of the schema complexity.
See also my comment here: https://news.ycombinator.com/item?id=16407707
Incoming data -> is it a valid messages at all? -> if it is a message of type x, hand it over to class X -> if it is a message of type y, hand it over to class Y -> etc
I found that to be a very easy way to setup a general-purpose processing pipeline.