Scalars🔗

In this section, we'll cover how GraphQL scalars work in Undine. Scalars are GraphQL types that represent concrete data types like strings, numbers, and booleans.

Built-in Scalars🔗

In addition to GraphQL's built-in scalars of Int, Float, String, Boolean, and ID, Undine provides its own scalars that are useful for representing common data types in Python.

Any🔗

Represent any value accepted by GraphQL. Used for e.g. for union types.

Base16🔗

Represents a base16-encoded string as defined in RFC 4648.

Base32🔗

Represents a base32-encoded string as defined in RFC 4648.

Base64🔗

Represents a base64-encoded string as defined in RFC 4648.

Date🔗

Represents a date value as specified by ISO 8601. Maps to the Python datetime.date type. See RFC 3339.

DateTime🔗

Represents a date and time value as specified by ISO 8601. Maps to the Python datetime.datetime type. See RFC 3339.

Decimal🔗

Represents a number as a string for correctly rounded floating point arithmetic. Maps to the Python decimal.Decimal type.

Duration🔗

Represents a duration of time in seconds. Maps to the Python datetime.timedelta type.

Email🔗

Represents a valid email address. See RFC 5322.

File🔗

Represents any kind of file. See the file upload section.

IP🔗

Represents a valid IPv4 or IPv6 address. See RFC 8200. and RFC 791.

IPv4🔗

Represents a valid IPv4 address. See RFC 791.

IPv6🔗

Represents a valid IPv6 address. See RFC 8200.

Image🔗

Represents an image file. See the file upload section.

JSON🔗

Represents a JSON serializable object. Maps to the Python dict type. See RFC 8259.

Null🔗

Represents represents an always null value. Maps to the Python None value.

Time🔗

Represents a time value as specified by ISO 8601. Maps to the Python datetime.time type. See RFC 3339.

URL🔗

Represents a valid URL. See RFC 3986.

UUID🔗

Represents a universally unique identifier string. Maps to Python's uuid.UUID type. See RFC 9562.

Modifying existing scalars🔗

All scalars have two functions that define its operation: parse and serialize These are used to parse incoming data to python types and serialize python data to GraphQL accepted types respectively.

In Undine's additional built-in scalars, these functions are single dispatch generic functions. This means that we can register different implementations for the functions which are called depending on the type of the input value — think of it like a dynamic switch statement. This allows us to replace or extend the behavior of a scalar depending on our use case.

For example, we might want to use the whenever library instead or in addition to python's built-in datetime. To do this, we can register a new implementation for the parse function of the DateTime scalar.

from whenever import Instant, PlainDateTime, ZonedDateTime

from undine.scalars.datetime import datetime_scalar


@datetime_scalar.parse.register
def _(value: str) -> ZonedDateTime:
    # Default "str" parse overridden to use 'whenever'
    return ZonedDateTime.parse_common_iso(value)


@datetime_scalar.serialize.register
def _(value: Instant | ZonedDateTime | PlainDateTime) -> str:
    # Extend serialization with types from 'whenever'.
    # Same implementation for all types in the union
    return value.format_common_iso()

Custom scalars🔗

We can also define our own scalars to represent types that cannot be represented by any of Undine's built-in scalars. Let's create a new scalar named Vector3 that represents a 3D vector using a tuple of three integers.

from django.core.exceptions import ValidationError

from undine.scalars import ScalarType

Vector3 = tuple[int, int, int]

# Create a new ScalarType for our custom scalar.
# In `ScalarType[Vector3, str]`, the first type parameter is the type that
# the scalar will parse to, and the second the type that it will serialize to.
vector3_scalar: ScalarType[Vector3, str] = ScalarType(
    name="Vector3",
    description="Represents a 3D vector as a string in format 'X,Y,Z'.",
)

# Create the GraphQLScalarType from graphql-core.
# This is the actual scalar we can add to our schema.
GraphQLVector3 = vector3_scalar.as_graphql_scalar()


# Register the parse and serialize functions for our scalar.
@vector3_scalar.parse.register
def _(value: str) -> Vector3:
    try:
        x, y, z = value.split(",")
        return int(x.strip()), int(y.strip()), int(z.strip())

    except ValueError as error:
        msg = f"Invalid vector format: {value}"
        raise ValidationError(msg) from error


@vector3_scalar.serialize.register
def _(value: tuple) -> str:
    if len(value) != 3:
        msg = f"Vector must have 3 components, got {len(value)}"
        raise ValidationError(msg)

    if not isinstance(value[0], int):
        msg = f"Vector component X is not an integer, got {value[0]}"
        raise ValidationError(msg)

    if not isinstance(value[1], int):
        msg = f"Vector component Y is not an integer, got {value[1]}"
        raise ValidationError(msg)

    if not isinstance(value[2], int):
        msg = f"Vector component Z is not an integer, got {value[2]}"
        raise ValidationError(msg)

    return f"{value[0]},{value[1]},{value[2]}"

If Vector3 corresponds to a Django model field, we could also let Undine know about it by registering it for its many built-in converters. This way a model field can be converted automatically to our scalar for QueryType Fields and MutationType Inputs. More on this in the "Hacking Undine" section.