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.