Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nullable & optional/required marks #807

Closed
raderio opened this issue Feb 14, 2019 · 6 comments
Closed

nullable & optional/required marks #807

raderio opened this issue Feb 14, 2019 · 6 comments

Comments

@raderio
Copy link

raderio commented Feb 14, 2019

Can we represent that a field is non-nullable & optional?
this mean, or we send a data for the field, or just do not send the field in request, null is not allowed.
{"foo": "bar", "baz": true} is allowed
{"baz": true} is allowed, because is optional
{"foo": null, "baz": true} is NOT allowed, because is non-nullable

Basically null has the meaning of "this property existed before, and I want to nullify its value". If there's no value, or the value needs not change (for requests, for example), it just shouldn't be sent.

This also is supported by OpenAPI and ReDoc.

@JakeWharton
Copy link
Member

For what datatype? You can use Optional and a simple @FromJson adapter.

@raderio
Copy link
Author

raderio commented Feb 14, 2019

For all datatypes. With Optional we only can handle if field is required or optional.
Bu field also can be nullable or non-nullable.

Int - required & non-nullable
Int? - required & nullable
Option - optional & non-nullable
Option<Int?> - optional & nullable

@JakeWharton
Copy link
Member

I meant for what Java/Kotlin type. Sounds like you need to write your own to model those dimensions as j.u.Optional won't.

@bradynpoulsen
Copy link

I went with a custom Omittable sealed class

import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract

sealed class Omittable<out T> {
    val provided get() = this !is Missing
    abstract val value: T

    object Missing : Omittable<Nothing>() {
        // have the IDE raise an error if the user knows a type is missing but still tries to access a value
        @Deprecated("Cannot access a missing value", level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("TODO(\"value is missing\")"))
        override val value: Nothing
            get() = error("cannot access provided field")
    }

    class Provided<out T>(override val value: T) : Omittable<T>()
}

@UseExperimental(ExperimentalContracts::class)
fun <T> Omittable<T>.isMissing(): Boolean {
    contract {
        returns(true) implies (this@isMissing is Omittable.Missing)
    }
    return !provided
}

@UseExperimental(ExperimentalContracts::class)
fun <T> Omittable<T>.isProvided(): Boolean {
    contract {
        returns(true) implies (this@isProvided is Omittable.Provided<*>)
    }
    return provided
}

and a custom accompanying adapter

import com.squareup.moshi.*
import java.lang.reflect.Type

class OmittableAdapter<T>(private val valueAdapter: JsonAdapter<T>) : JsonAdapter<Omittable<T>>() {
    @Suppress("UNCHECKED_CAST")
    override fun fromJson(reader: JsonReader) = Omittable.Provided(valueAdapter.fromJson(reader) as T)

    override fun toJson(writer: JsonWriter, value: Omittable<T>?) {
        when (value) {
            is Omittable.Missing -> writer.nullValue()
            is Omittable.Provided -> valueAdapter.toJson(writer, value.value)
        }
    }

    companion object Factory : JsonAdapter.Factory {
        override fun create(type: Type, annotations: Set<out Annotation>, moshi: Moshi): JsonAdapter<*>? {
            return if (Types.getRawType(type) == Omittable::class.java && annotations.isEmpty()) {
                val valueType = Types.collectionElementType(type, Omittable::class.java)
                return OmittableAdapter(moshi.adapter<Any>(valueType).nullSafe())
            } else {
                null
            }
        }
    }
}

// we don't actually use this, just defined to illustrate how it is registered to the builder
fun Moshi.Builder.addOmittableAdapter() {
    add(OmittableAdapter)
}

@bradynpoulsen
Copy link

bradynpoulsen commented Feb 27, 2019

Then in my data class that I deserialize to, I default the omittable values to Missing

data class A(
    val foo: Omittable<String> = Omittable.Missing,
    val baz: Boolean
)

@swankjesse
Copy link
Member

No action to take on this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants