jsonPointer

inline fun <T> jsonPointer(property: KProperty1<T, *>): String(source)

Builds the JSON Pointer (RFC 6901) that identifies property in T's serialized form, in the URI fragment notation RFC 9457's own example uses: #/age.

Preferred over writing the pointer as a string, because renaming the property is then a compile error rather than a pointer that silently names a member no longer there.

The property's own name is the wire name only when nothing renames it. If the class carries @SerialName on that property, or the application's Json applies a JsonNamingStrategy, this function cannot see it — the mapping from a Kotlin property to a serial name is not reachable without kotlin-reflect, which this library does not use. @SerialName is at least detected: the name is checked against T's serial descriptor and an unknown one throws. A naming strategy is not detectable at all, and a project using one should build its pointers from strings instead.

Throws

if T's serialized form has no member under the property's name.

Samples

install(RequestValidation) {
    validate<Customer> { customer ->
        val errors =
            buildList {
                if (customer.age <= 0) {
                    add(jsonPointer(Customer::age) to "must be a positive integer")
                }
                // No property reference reaches a nested member, but every segment of the path is
                // still checked against the descriptor it belongs to.
                if (customer.profile.color !in setOf("green", "red", "blue")) {
                    add(jsonPointer<Customer>("profile", "color") to "must be 'green', 'red' or 'blue'")
                }
                // `@SerialName` renames this one, so the pointer has to name what the wire
                // carries. `jsonPointer(Customer::emailAddress)` would throw rather than guess.
                if ("@" !in customer.emailAddress) {
                    add(jsonPointer<Customer>("email_address") to "must be an email address")
                }
            }

        if (errors.isEmpty()) ValidationResult.Valid else invalidFields(errors)
    }
}

install(StatusPages) {
    problemDetails {
        requestValidation(ValidationError)
    }
}

routing {
    post("/customers") {
        call.respond(call.receive<Customer>())
    }
}

inline fun <T> jsonPointer(vararg path: String): String(source)

Builds the JSON Pointer (RFC 6901) for path in T's serialized form, in the URI fragment notation RFC 9457's own example uses: jsonPointer<Customer>("profile", "color") gives #/profile/color.

Every segment is checked against the serial descriptor it belongs to, so the whole path has to exist. Use this form for a nested member, for a member renamed by @SerialName — the segments are serial names, which is what the wire carries — and wherever the path is assembled rather than written out. Segments are escaped for you: ~ and / per RFC 6901 §3, then whatever a URI fragment does not admit is percent-encoded.

A list segment is its index, or - for the position past the end, as RFC 6901 §4 defines. Under a map, any segment is accepted, since map keys are not known statically. Under a polymorphic or contextual member the shape is unknown, so checking stops there and the rest of the path is taken on trust.

Throws

if path is empty, or if a segment names nothing in the descriptor it is resolved against.

Samples

install(RequestValidation) {
    validate<Customer> { customer ->
        val errors =
            buildList {
                if (customer.age <= 0) {
                    add(jsonPointer(Customer::age) to "must be a positive integer")
                }
                // No property reference reaches a nested member, but every segment of the path is
                // still checked against the descriptor it belongs to.
                if (customer.profile.color !in setOf("green", "red", "blue")) {
                    add(jsonPointer<Customer>("profile", "color") to "must be 'green', 'red' or 'blue'")
                }
                // `@SerialName` renames this one, so the pointer has to name what the wire
                // carries. `jsonPointer(Customer::emailAddress)` would throw rather than guess.
                if ("@" !in customer.emailAddress) {
                    add(jsonPointer<Customer>("email_address") to "must be an email address")
                }
            }

        if (errors.isEmpty()) ValidationResult.Valid else invalidFields(errors)
    }
}

install(StatusPages) {
    problemDetails {
        requestValidation(ValidationError)
    }
}

routing {
    post("/customers") {
        call.respond(call.receive<Customer>())
    }
}