openlang

Validating JSON before the discriminated union

A type assertion is not a parser. An external response starts as bytes and says nothing about its variant until something checks it.

Text decoding and JSON parsing come first, each with its own failure path. The parsed value should reach validation as unknown.

The supported response is either a ready tag with an items array of strings or a failed tag with a message string. The runtime check has to confirm three things. The value is an object, its tag is one we know, and the payload for that tag has the required shape. An object with ready and no items is incomplete. One with waiting carries a tag we never declared. Writing as with the union fixes neither.

Once the check passes, the decoder returns a discriminated union. Testing for ready narrows it, and the items become usable. An exhaustive check with never points out an unhandled alternative in the declared model. Good maintenance information, but it never looks at response bytes.

Rejected input still needs a decision. Report a decoding failure, or model unknown responses as a variant on purpose. Casting quietly into the accepted union skips that choice.

Keep the runtime evidence right next to where the typed value gets built. Review each handler's calculation too. A covered branch can still compute the wrong answer.

The conversation continues 5

Follow the reasoning
  1. Permalink

    An Either describes which forms a result can take. It says nothing about whether evaluation ever produces one.

    A function that matches Right _ and returns True accepts Right undefined without touching the payload. Ask for the length of that payload and evaluation does need it, and fails right there. And let loop = loop in loop can be given the same type while never reaching either constructor.

    So under your split between checking input and representing alternatives, Haskell has one more layer. The decoder can be right and the type precise, and a caller can still demand a value that isn't there. That tells us the result's shape, if evaluation reaches it.

  2. Permalink

    Add Deferred(ticket) as a third case and follow where the change spreads. A closed Scala 3 enum gives the case its own data, which is nice, but now the decoder changes and so does every consumer, rendering and persistence included. That coordination pays off while one model owns all the alternatives.

    It turns into a cost once independent producers need to add variants without touching that central definition. Then an open interface, or an unknown envelope that gets explicitly validated, may fit better. You give up some central exhaustiveness for it.

  3. Permalink

    What does absence mean for ticket? A missing or null ticket could be a malformed response. It could also be an accepted deferral that just has no tracking reference. Kotlin's String? would at least make the absence visible, but it still doesn't say which of the two it is. Leave the field nullable and every consumer of Deferred(ticket) decides on its own. I would make this boundary explicit before the case goes in.

  4. Permalink

    Malformed, for this extension. deferred needs a ticket that is present and holds a nonempty string. A missing property fails decoding, so does null, so does "". That's the acceptance rule, and it lives in the decoder, right next to the tag check.

    The empty case people usually want already has a home. ready with items equal to [] means the request completed with nothing in it. That's a real result. A deferral with no ticket is a different thing and shouldn't borrow that shape.

    If the service later allows untracked deferral, that's a new alternative with its own name and its own note for callers. What I don't want is Deferred built quietly out of absence because the field happened to be optional. While the protocol is still moving, a separate variant is cheap to add and cheap to drop.

Other sides of the same question

  1. Is a read-only List<Int> a snapshot?

    List<Int> is a read-only view, not a snapshot. It tells a consumer which operations it can call through that reference. It pr…

    Replies 2kotlin