← Back to Home

Enum Serialization Guide

Serialization · JSON · API Integration

Complete Guide to Enum Conversion & Serialization

Enum ←→ String / JSON

Converting enums to and from strings and JSON is a common requirement in modern application development. Whether it's API data exchange, database storage, or frontend-backend communication, proper enum serialization is crucial. This guide compiles conversion methods, common pitfalls, and best practices across mainstream languages.

Covers languages including C#, Python, Java, TypeScript, Rust, Go, and enum integration with API specifications like OpenAPI, JSON Schema, and GraphQL.

Serialization Fundamentals

Understand the core concepts and common scenarios of enum serialization to build a foundation for deeper learning.

What is Enum Serialization?

Enum serialization is the process of converting enum types into storable and transmittable formats (such as strings, numbers, JSON). Deserialization is the reverse process of restoring these formats back to enum types.

// Serialization: Enum → String
OrderStatus.PENDING → "pending"

// Deserialization: String → Enum  
"pending" → OrderStatus.PENDING

// JSON Serialization
{ "status": OrderStatus.PENDING }
→ { "status": "pending" }

// JSON Deserialization
{ "status": "pending" }
→ { "status": OrderStatus.PENDING }

Why Serialization is Needed

  • API Data Exchange

    Pass enum values between frontend and backend via JSON

  • Database Storage

    Save enums as strings or integers

  • Logging

    Record enum states in readable format

  • Configuration Files

    Use enums in YAML/JSON configurations

  • Cross-Language Communication

    Data transfer between different technology stacks

Common Serialization Strategies

String Serialization

Recommended for APIs and configuration

"pending"
"confirmed"
"shipped"

Pros: Good readability, easy to debug, cross-language friendly

Integer Serialization

For performance-sensitive scenarios

0  // PENDING
1  // CONFIRMED
2  // SHIPPED

Pros: Space-efficient, good performance
Cons: Not intuitive, hard to debug

Object Serialization

Includes additional metadata

{
  "code": "pending",
  "label": "Pending",
  "order": 0
}

Pros: Information-rich
Cons: Large size, complex

Multi-Language Implementation Comparison

Enum serialization and deserialization implementation methods across mainstream programming languages.

C# Enum Conversion

Enum → String

enum OrderStatus { Pending, Confirmed, Shipped }

// Method 1: ToString()
OrderStatus status = OrderStatus.Pending;
string str = status.ToString();  // "Pending"

// Method 2: Enum.GetName()
string str = Enum.GetName(typeof(OrderStatus), status);

// Method 3: Lowercase conversion
string str = status.ToString().ToLower();  // "pending"

// JSON Serialization (System.Text.Json)
using System.Text.Json;
using System.Text.Json.Serialization;

var options = new JsonSerializerOptions
{
    Converters = { new JsonStringEnumConverter() }
};
string json = JsonSerializer.Serialize(status, options);
// "Pending"

// Newtonsoft.Json
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

[JsonConverter(typeof(StringEnumConverter))]
public OrderStatus Status { get; set; }

string json = JsonConvert.SerializeObject(order);
// { "status": "Pending" }

String → Enum

// Method 1: Enum.Parse()
string str = "Pending";
OrderStatus status = (OrderStatus)Enum.Parse(
    typeof(OrderStatus), str);

// Method 2: Enum.TryParse() (Recommended)
if (Enum.TryParse(str, out var status))
{
    // Parse successful
}

// Method 3: Ignore case
Enum.TryParse(
    str, ignoreCase: true, out var status);

// JSON Deserialization
var order = JsonSerializer.Deserialize(
    json, options);

// Handle invalid values
if (!Enum.IsDefined(typeof(OrderStatus), status))
{
    throw new ArgumentException("Invalid status");
}

Python Enum Conversion

Enum → String

from enum import Enum

class OrderStatus(str, Enum):
    PENDING = "pending"
    CONFIRMED = "confirmed"
    SHIPPED = "shipped"

status = OrderStatus.PENDING

# Method 1: .value attribute
str_value = status.value  # "pending"

# Method 2: .name attribute (enum name)
name = status.name  # "PENDING"

# Method 3: str() conversion
str_value = str(status.value)

# JSON Serialization (built-in json)
import json

data = {"status": status.value}
json_str = json.dumps(data)
# '{"status": "pending"}'

# Using custom JSONEncoder
class EnumEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, Enum):
            return obj.value
        return super().default(obj)

json_str = json.dumps(
    {"status": status}, 
    cls=EnumEncoder
)

# Pydantic automatic serialization
from pydantic import BaseModel

class Order(BaseModel):
    status: OrderStatus

order = Order(status=OrderStatus.PENDING)
json_str = order.model_dump_json()
# '{"status":"pending"}'

String → Enum

# Method 1: Direct construction
str_value = "pending"
status = OrderStatus(str_value)

# Method 2: By name
status = OrderStatus["PENDING"]

# Method 3: Safe parsing
def parse_status(value: str) -> OrderStatus:
    try:
        return OrderStatus(value)
    except ValueError:
        raise ValueError(f"Invalid status: {value}")

# Method 4: With default value
def parse_status_safe(
    value: str, 
    default: OrderStatus = OrderStatus.PENDING
) -> OrderStatus:
    try:
        return OrderStatus(value)
    except ValueError:
        return default

# JSON Deserialization
data = json.loads(json_str)
status = OrderStatus(data["status"])

# Pydantic automatic deserialization
order = Order.model_validate_json(json_str)
# order.status is OrderStatus.PENDING

# Validate validity
def is_valid_status(value: str) -> bool:
    try:
        OrderStatus(value)
        return True
    except ValueError:
        return False

Java Enum Conversion

Enum → String

public enum OrderStatus {
    PENDING("pending"),
    CONFIRMED("confirmed"),
    SHIPPED("shipped");
    
    private final String value;
    
    OrderStatus(String value) {
        this.value = value;
    }
    
    public String getValue() {
        return value;
    }
}

OrderStatus status = OrderStatus.PENDING;

// Method 1: name() - enum name
String name = status.name();  // "PENDING"

// Method 2: getValue() - custom value
String value = status.getValue();  // "pending"

// Method 3: toString() (can be overridden)
String str = status.toString();

// JSON Serialization (Jackson)
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.annotation.JsonValue;

public enum OrderStatus {
    PENDING("pending"),
    CONFIRMED("confirmed");
    
    @JsonValue  // Use this method for serialization
    public String getValue() {
        return value;
    }
}

ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(status);
// "pending"

// Gson Serialization
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;

public enum OrderStatus {
    @SerializedName("pending")
    PENDING,
    @SerializedName("confirmed")
    CONFIRMED
}

String → Enum

// Method 1: valueOf() - by name
String name = "PENDING";
OrderStatus status = OrderStatus.valueOf(name);

// Method 2: Custom fromValue() method
public enum OrderStatus {
    PENDING("pending"),
    CONFIRMED("confirmed");
    
    private final String value;
    
    public static OrderStatus fromValue(String value) {
        for (OrderStatus s : values()) {
            if (s.value.equals(value)) {
                return s;
            }
        }
        throw new IllegalArgumentException(
            "Unknown status: " + value
        );
    }
}

OrderStatus status = OrderStatus.fromValue("pending");

// Method 3: Using Stream API
public static Optional fromValue(
    String value
) {
    return Arrays.stream(values())
        .filter(s -> s.value.equals(value))
        .findFirst();
}

// JSON Deserialization (Jackson)
import com.fasterxml.jackson.annotation.JsonCreator;

public enum OrderStatus {
    PENDING("pending");
    
    @JsonCreator  // Use this method for deserialization
    public static OrderStatus fromValue(String value) {
        return fromValue(value);
    }
}

Order order = mapper.readValue(json, Order.class);

// Safe parsing
public static OrderStatus parse(
    String value, 
    OrderStatus defaultValue
) {
    try {
        return fromValue(value);
    } catch (IllegalArgumentException e) {
        return defaultValue;
    }
}

TypeScript Enum Conversion

Enum → String

// String enum (recommended)
enum OrderStatus {
  Pending = "pending",
  Confirmed = "confirmed",
  Shipped = "shipped"
}

const status = OrderStatus.Pending;

// Method 1: Direct use (string enum)
const str: string = status;  // "pending"

// Method 2: Explicit conversion
const str = status.toString();

// JSON Serialization (automatic)
const data = { status };
const json = JSON.stringify(data);
// '{"status":"pending"}'

// Numeric enum conversion
enum NumericStatus {
  Pending,     // 0
  Confirmed,   // 1
  Shipped      // 2
}

const status = NumericStatus.Pending;

// Get enum name
const name = NumericStatus[status];  // "Pending"

// Using mapping object
const statusMap = {
  [NumericStatus.Pending]: "pending",
  [NumericStatus.Confirmed]: "confirmed",
  [NumericStatus.Shipped]: "shipped"
};

const str = statusMap[status];  // "pending"

// Using as const (type-safe strings)
const OrderStatus = {
  Pending: "pending",
  Confirmed: "confirmed",
  Shipped: "shipped"
} as const;

type OrderStatus = 
  typeof OrderStatus[keyof typeof OrderStatus];

const status: OrderStatus = "pending";

String → Enum

// Method 1: Type assertion (string enum)
const str = "pending";
const status = str as OrderStatus;

// Method 2: Convert after validation
function parseStatus(value: string): OrderStatus {
  if (Object.values(OrderStatus).includes(
    value as OrderStatus
  )) {
    return value as OrderStatus;
  }
  throw new Error(`Invalid status: ${value}`);
}

// Method 3: Type guard
function isOrderStatus(
  value: string
): value is OrderStatus {
  return Object.values(OrderStatus).includes(
    value as OrderStatus
  );
}

if (isOrderStatus(str)) {
  const status: OrderStatus = str;
}

// JSON Deserialization
interface Order {
  status: OrderStatus;
}

const order: Order = JSON.parse(json);
// Validation required

// Using Zod validation
import { z } from "zod";

const OrderStatusSchema = z.enum([
  "pending",
  "confirmed", 
  "shipped"
]);

const OrderSchema = z.object({
  status: OrderStatusSchema
});

const order = OrderSchema.parse(JSON.parse(json));

// Numeric enum deserialization
const numericValue = 0;
const status = numericValue as NumericStatus;

// Parse by name
const name = "Pending";
const status = OrderStatus[
  name as keyof typeof OrderStatus
];

Rust Enum Conversion

Enum → String

use serde::{Serialize, Deserialize};

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum OrderStatus {
    Pending,
    Confirmed,
    Shipped,
}

// Method 1: Display trait
use std::fmt;

impl fmt::Display for OrderStatus {
    fn fmt(&self, f: &mut fmt::Formatter) 
        -> fmt::Result {
        match self {
            OrderStatus::Pending => 
                write!(f, "pending"),
            OrderStatus::Confirmed => 
                write!(f, "confirmed"),
            OrderStatus::Shipped => 
                write!(f, "shipped"),
        }
    }
}

let status = OrderStatus::Pending;
let str = status.to_string();  // "pending"

// Method 2: Manual implementation
impl OrderStatus {
    pub fn as_str(&self) -> &str {
        match self {
            OrderStatus::Pending => "pending",
            OrderStatus::Confirmed => "confirmed",
            OrderStatus::Shipped => "shipped",
        }
    }
}

let str = status.as_str();

// JSON Serialization (serde_json)
let json = serde_json::to_string(&status)?;
// "\"pending\""

let order = Order { 
    status: OrderStatus::Pending 
};
let json = serde_json::to_string(&order)?;
// "{\"status\":\"pending\"}"

String → Enum

// Method 1: FromStr trait
use std::str::FromStr;

impl FromStr for OrderStatus {
    type Err = String;
    
    fn from_str(s: &str) -> Result {
        match s {
            "pending" => Ok(OrderStatus::Pending),
            "confirmed" => Ok(OrderStatus::Confirmed),
            "shipped" => Ok(OrderStatus::Shipped),
            _ => Err(format!("Invalid status: {}", s)),
        }
    }
}

let str = "pending";
let status = OrderStatus::from_str(str)?;

// Or use parse()
let status: OrderStatus = str.parse()?;

// Method 2: Using strum
use strum_macros::{EnumString, Display};

#[derive(EnumString, Display)]
#[strum(serialize_all = "lowercase")]
pub enum OrderStatus {
    Pending,
    Confirmed,
    Shipped,
}

let status: OrderStatus = "pending".parse()?;

// JSON Deserialization
let status: OrderStatus = 
    serde_json::from_str("\"pending\"")?;

let order: Order = serde_json::from_str(json)?;

// Custom deserialization
#[derive(Deserialize)]
struct Order {
    #[serde(deserialize_with = "parse_status")]
    status: OrderStatus,
}

fn parse_status<'de, D>(
    deserializer: D
) -> Result
where
    D: serde::Deserializer<'de>,
{
    let s = String::deserialize(deserializer)?;
    OrderStatus::from_str(&s)
        .map_err(serde::de::Error::custom)
}

Go Enum Conversion

Enum → String

package main

type OrderStatus string

const (
    OrderStatusPending   OrderStatus = "pending"
    OrderStatusConfirmed OrderStatus = "confirmed"
    OrderStatusShipped   OrderStatus = "shipped"
)

status := OrderStatusPending

// Method 1: Direct conversion (type alias)
str := string(status)  // "pending"

// Method 2: String() method
func (s OrderStatus) String() string {
    return string(s)
}

str := status.String()

// JSON Serialization (automatic)
import "encoding/json"

type Order struct {
    Status OrderStatus `json:"status"`
}

order := Order{Status: OrderStatusPending}
json, _ := json.Marshal(order)
// {"status":"pending"}

// Using integer enum
type NumericStatus int

const (
    Pending NumericStatus = iota
    Confirmed
    Shipped
)

// Implement MarshalJSON
func (s NumericStatus) MarshalJSON() (
    []byte, error
) {
    statuses := []string{
        "pending", "confirmed", "shipped"
    }
    if int(s) < len(statuses) {
        return json.Marshal(statuses[s])
    }
    return nil, fmt.Errorf("invalid status")
}

String → Enum

// Method 1: Direct conversion
str := "pending"
status := OrderStatus(str)

// Method 2: Validation function
func ParseOrderStatus(s string) (
    OrderStatus, error
) {
    switch s {
    case "pending":
        return OrderStatusPending, nil
    case "confirmed":
        return OrderStatusConfirmed, nil
    case "shipped":
        return OrderStatusShipped, nil
    default:
        return "", fmt.Errorf(
            "invalid status: %s", s
        )
    }
}

status, err := ParseOrderStatus(str)

// Method 3: Using map
var statusMap = map[string]OrderStatus{
    "pending":   OrderStatusPending,
    "confirmed": OrderStatusConfirmed,
    "shipped":   OrderStatusShipped,
}

func ParseStatus(s string) (
    OrderStatus, bool
) {
    status, ok := statusMap[s]
    return status, ok
}

// JSON Deserialization
var order Order
err := json.Unmarshal(jsonData, &order)

// Custom UnmarshalJSON
func (s *NumericStatus) UnmarshalJSON(
    data []byte
) error {
    var str string
    if err := json.Unmarshal(data, &str); 
        err != nil {
        return err
    }
    
    switch str {
    case "pending":
        *s = Pending
    case "confirmed":
        *s = Confirmed
    case "shipped":
        *s = Shipped
    default:
        return fmt.Errorf("invalid status")
    }
    return nil
}

API Integration & Specifications

Properly use enums in different architectures like REST API, GraphQL, gRPC, etc., to ensure consistency and type safety in frontend-backend data exchange.

API Enum Design Principles

Consistency

Frontend and backend use the same enum values, maintaining unified naming and format

Extensibility

Consider future enum additions during design, use strings instead of numbers

Documentation

Clearly list all possible values and their meanings in API documentation

OpenAPI / Swagger

Standard enum definition and documentation generation for RESTful APIs

REST API

Use Cases

  • • RESTful API design and documentation
  • • Frontend-backend contract definition
  • • Automatic client SDK generation
  • • API testing and mock data

OpenAPI 3.0 Enum Definition

openapi: 3.0.0
info:
  title: Order API
  version: 1.0.0

components:
  schemas:
    OrderStatus:
      type: string
      enum:
        - pending
        - confirmed
        - shipped
        - delivered
        - cancelled
      description: Order status
      example: pending
    
    Order:
      type: object
      properties:
        id:
          type: integer
        status:
          $ref: '#/components/schemas/OrderStatus'
        items:
          type: array
          items:
            type: object
      required:
        - id
        - status

paths:
  /orders:
    post:
      summary: Create order
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Order'
            example:
              id: 123
              status: pending
      responses:
        '201':
          description: Order created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'

REST API Implementation Examples

GET /api/orders?status=pending

{
  "data": [
    {
      "id": 123,
      "status": "pending",
      "createdAt": "2024-01-01T00:00:00Z"
    }
  ],
  "meta": {
    "total": 1
  }
}

PATCH /api/orders/123

// Request
{
  "status": "confirmed"
}

// Response (200 OK)
{
  "id": 123,
  "status": "confirmed",
  "updatedAt": "2024-01-01T00:01:00Z"
}

// Error Response (400 Bad Request)
{
  "error": "INVALID_ENUM_VALUE",
  "message": "Invalid status value",
  "allowedValues": [
    "pending", "confirmed", "shipped"
  ]
}

Auto-generated Client Code

TypeScript (openapi-generator)

export enum OrderStatus {
  Pending = 'pending',
  Confirmed = 'confirmed',
  Shipped = 'shipped'
}

export interface Order {
  id: number;
  status: OrderStatus;
}

// Type-safe API calls
const order = await api.updateOrder(123, {
  status: OrderStatus.Confirmed
});

Python (openapi-generator)

class OrderStatus(str, Enum):
    PENDING = "pending"
    CONFIRMED = "confirmed"
    SHIPPED = "shipped"

# Type hints and validation
def update_order(
    order_id: int, 
    status: OrderStatus
) -> Order:
    ...

OpenAPI Best Practices

  • ✓ Use $ref to reference enum definitions, avoid duplication
  • ✓ Add description and example to improve documentation readability
  • ✓ Provide default values default for enum values
  • ✓ When returning errors in responses, list all allowed enum values
  • ✓ Use code generation tools to keep frontend and backend in sync

JSON Schema

Powerful JSON data validation and documentation tool

Data Validation

Use Cases

  • • Frontend form validation
  • • API request/response validation
  • • Configuration file validation
  • • Data import/export validation

Schema Definition

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Order",
  "type": "object",
  "properties": {
    "id": {
      "type": "integer"
    },
    "status": {
      "type": "string",
      "enum": [
        "pending",
        "confirmed",
        "shipped",
        "delivered",
        "cancelled"
      ],
      "description": "Order status",
      "default": "pending"
    },
    "priority": {
      "type": "string",
      "enum": ["low", "medium", "high"],
      "enumNames": ["Low", "Medium", "High"]
    }
  },
  "required": ["id", "status"],
  "definitions": {
    "OrderStatus": {
      "type": "string",
      "enum": [
        "pending",
        "confirmed",
        "shipped"
      ]
    }
  }
}

Validation Examples

// JavaScript (Ajv)
import Ajv from "ajv";

const ajv = new Ajv();
const validate = ajv.compile(schema);

const data = {
  id: 123,
  status: "pending"
};

const valid = validate(data);
if (!valid) {
  console.log(validate.errors);
}

// Python (jsonschema)
from jsonschema import validate, ValidationError

data = {
    "id": 123,
    "status": "pending"
}

try:
    validate(instance=data, schema=schema)
except ValidationError as e:
    print(f"Validation error: {e.message}")

// TypeScript (Zod)
import { z } from "zod";

const OrderStatusSchema = z.enum([
  "pending",
  "confirmed",
  "shipped",
  "delivered",
  "cancelled"
]);

const OrderSchema = z.object({
  id: z.number(),
  status: OrderStatusSchema.default("pending")
});

type Order = z.infer;

const order = OrderSchema.parse(data);

JSON Schema Best Practices

  • ✓ Use enumNames to provide friendly display names for enum values
  • ✓ Define reusable enum types in definitions
  • ✓ Combine with UI Schema to generate form selection controls
  • ✓ Use modern libraries like Zod for better TypeScript support
  • ✓ Validate on both client and server side

GraphQL

Type-safe query language with built-in enum support

Query Language

Use Cases

  • • Modern API development
  • • Precise data queries
  • • Strongly typed schema definitions
  • • Automatic documentation and type generation

Schema Definition

type Query {
  orders(status: OrderStatus): [Order!]!
  order(id: ID!): Order
}

type Mutation {
  createOrder(input: CreateOrderInput!): Order!
  updateOrderStatus(
    id: ID!, 
    status: OrderStatus!
  ): Order!
}

enum OrderStatus {
  PENDING
  CONFIRMED
  SHIPPED
  DELIVERED
  CANCELLED
}

type Order {
  id: ID!
  status: OrderStatus!
  items: [OrderItem!]!
  createdAt: DateTime!
}

input CreateOrderInput {
  status: OrderStatus = PENDING
  items: [OrderItemInput!]!
}

# Enum with descriptions
enum OrderStatus {
  """Pending"""
  PENDING
  
  """Confirmed"""
  CONFIRMED
  
  """Shipped"""
  SHIPPED
  
  """Delivered"""
  DELIVERED
  
  """Cancelled"""
  CANCELLED
}

Resolver Implementation

// TypeScript (Apollo Server)
enum OrderStatus {
  PENDING = "PENDING",
  CONFIRMED = "CONFIRMED",
  SHIPPED = "SHIPPED",
  DELIVERED = "DELIVERED",
  CANCELLED = "CANCELLED"
}

const resolvers = {
  Query: {
    orders: (
      parent, 
      { status }, 
      context
    ) => {
      return context.db.orders.findMany({
        where: { status }
      });
    }
  },
  Mutation: {
    updateOrderStatus: (
      parent, 
      { id, status }, 
      context
    ) => {
      return context.db.orders.update({
        where: { id },
        data: { status }
      });
    }
  }
};

// Python (Graphene)
import graphene

class OrderStatus(graphene.Enum):
    PENDING = "pending"
    CONFIRMED = "confirmed"
    SHIPPED = "shipped"
    DELIVERED = "delivered"
    CANCELLED = "cancelled"

class Order(graphene.ObjectType):
    id = graphene.ID()
    status = graphene.Field(OrderStatus)

class Query(graphene.ObjectType):
    orders = graphene.List(
        Order,
        status=graphene.Argument(OrderStatus)
    )

GraphQL Best Practices

  • ✓ Use uppercase for enum values, consistent with GraphQL conventions
  • ✓ Use triple quotes """ to add descriptions
  • ✓ Provide default values in Input types
  • ✓ Use code generation tools to sync TypeScript/Python types
  • ✓ Validate enum value validity in Resolvers

gRPC / Protocol Buffers

High-performance RPC framework, suitable for microservice communication

RPC

Use Cases

  • • Inter-service communication
  • • High-performance RPC calls
  • • Strongly typed interface definitions
  • • Cross-language service integration

Proto Definition

syntax = "proto3";

package order;

enum OrderStatus {
  // First value must be 0
  ORDER_STATUS_UNSPECIFIED = 0;
  ORDER_STATUS_PENDING = 1;
  ORDER_STATUS_CONFIRMED = 2;
  ORDER_STATUS_SHIPPED = 3;
  ORDER_STATUS_DELIVERED = 4;
  ORDER_STATUS_CANCELLED = 5;
}

message Order {
  int64 id = 1;
  OrderStatus status = 2;
  repeated OrderItem items = 3;
}

message CreateOrderRequest {
  OrderStatus status = 1;
  repeated OrderItem items = 2;
}

message UpdateStatusRequest {
  int64 id = 1;
  OrderStatus status = 2;
}

service OrderService {
  rpc CreateOrder(CreateOrderRequest) 
    returns (Order);
  rpc UpdateStatus(UpdateStatusRequest) 
    returns (Order);
  rpc ListOrders(ListOrdersRequest) 
    returns (ListOrdersResponse);
}

Usage Examples

// Go (protoc-gen-go)
import pb "path/to/generated/order"

// Create order
req := &pb.CreateOrderRequest{
    Status: pb.OrderStatus_ORDER_STATUS_PENDING,
}

order, err := client.CreateOrder(ctx, req)

// Check status
if order.Status == 
    pb.OrderStatus_ORDER_STATUS_SHIPPED {
    // Handle shipped orders
}

// Python (grpcio)
import order_pb2

# Create order
request = order_pb2.CreateOrderRequest(
    status=order_pb2.ORDER_STATUS_PENDING
)

order = stub.CreateOrder(request)

# Check status
if order.status == \
    order_pb2.ORDER_STATUS_SHIPPED:
    # Handle shipped orders
    pass

// TypeScript (ts-proto)
import { 
  OrderStatus, 
  CreateOrderRequest 
} from "./generated/order";

const request: CreateOrderRequest = {
  status: OrderStatus.ORDER_STATUS_PENDING,
  items: []
};

const order = await client.createOrder(request);

if (order.status === 
    OrderStatus.ORDER_STATUS_SHIPPED) {
  // Handle shipped orders
}

gRPC Best Practices

  • ✓ First enum value must be 0, typically named UNSPECIFIED
  • ✓ Use prefixes to avoid naming conflicts (e.g., ORDER_STATUS_)
  • ✓ Add descriptions in comments for team understanding
  • ✓ Use protoc to generate code for multiple languages
  • ✓ Be cautious when deleting or renaming enum values, consider backward compatibility

API Comparison Matrix

Feature REST/OpenAPI GraphQL gRPC
Enum Format String String (uppercase) Integer
Documentation Excellent Excellent Moderate
Performance Moderate Moderate Excellent
Type Safety Tool-dependent Built-in Built-in
Browser Support Native Native Requires proxy
Learning Curve Low Medium Medium

Selection advice: Use REST/OpenAPI for public APIs, GraphQL for frontend applications, gRPC for inter-service communication

Best Practices & Common Pitfalls

Avoid common mistakes and adopt proven serialization strategies.

Common Pitfalls

Pitfall 1: Case Inconsistency

// Frontend sends
{ "status": "Pending" }

// Backend expects
{ "status": "pending" }

// Result: Deserialization fails

Solution: Use lowercase consistently or specify case rules

Pitfall 2: Numeric Serialization Ambiguity

// Database stores: 1 represents CONFIRMED
// New requirement inserts status between PENDING and CONFIRMED
// Causes data inconsistency

Solution: Always use strings instead of numeric indices

Pitfall 3: Unvalidated Input

// Directly convert unknown string
const status = input as OrderStatus;
// May cause runtime errors

Solution: Always validate input is a valid enum value

Pitfall 4: Ignoring null/undefined

{ "status": null }

// Not handling null case
const status = OrderStatus(data["status"])
// Throws exception

Solution: Handle null/undefined, provide default values

Best Practices

Practice 1: Use String Enums

// TypeScript
enum Status { 
  Pending = "pending" 
}

// Python
class Status(str, Enum):
    PENDING = "pending"

Benefits: Readable, stable, easy to debug

Practice 2: Unified Naming Convention

// API uses snake_case
"pending_payment"

// Frontend uses camelCase
PendingPayment

// Backend uses UPPER_SNAKE_CASE
PENDING_PAYMENT

Convert at serialization layer

Practice 3: Add Validation Layer

function parseStatus(
  value: unknown
): OrderStatus {
  if (!isValidStatus(value)) {
    throw new ValidationError(
      `Invalid status: ${value}`
    );
  }
  return value as OrderStatus;
}

Always validate external input

Practice 4: Use Schema Validation

// OpenAPI, JSON Schema, Zod
// Auto-generate validation code
const OrderSchema = z.object({
  status: z.enum([
    "pending", "confirmed"
  ])
});

Leverage tools for automated validation

Serialization Strategy Decision Tree

Scenario 1: Public API

→ Use string enums

→ Provide OpenAPI documentation

→ Version APIs to avoid breaking changes

Reason: Readability, stability, documentation

Scenario 2: Internal Microservices

→ Use gRPC + Protocol Buffers

→ Numeric enums (performance priority)

→ Strict schema definitions

Reason: Performance, type safety, code generation

Scenario 3: Database Storage

→ String enums (PostgreSQL, MySQL)

→ Or use Lookup Table

→ Add migration scripts

Reason: Easy maintenance, easy querying, extensible

Scenario 4: Frontend State Management

→ TypeScript string enums

→ Or as const objects

→ Sync with backend API schema

Reason: Type safety, autocomplete, refactoring-friendly

Testing Checklist

Serialization Tests

  • ✓ All enum values serialize correctly
  • ✓ Serialized format conforms to API specification
  • ✓ Case handling is correct
  • ✓ Special character handling (e.g., underscores, hyphens)
  • ✓ null/undefined handling
  • ✓ Enums in nested objects
  • ✓ Enums in arrays

Deserialization Tests

  • ✓ Valid values parse correctly
  • ✓ Invalid values throw errors or return defaults
  • ✓ Case tolerance (if needed)
  • ✓ Empty string handling
  • ✓ null/undefined handling
  • ✓ Clear error messages
  • ✓ Backward compatibility (adding new values)

Tools & Resources

Use tools to validate serialization compatibility, generate code and documentation.

Recommended Resources

Code Generation Tools

  • • OpenAPI Generator - Generate clients from OpenAPI
  • • Swagger Codegen - Multi-language code generation
  • • protoc - Protocol Buffers compiler
  • • GraphQL Code Generator - GraphQL type generation

Validation Libraries

  • • Zod (TypeScript) - Schema validation
  • • Pydantic (Python) - Data validation
  • • Joi (Node.js) - Object validation
  • • Ajv (JavaScript) - JSON Schema validation

Summary: Enum Serialization Best Practices

1

Choose the Right Strategy

Use strings for public APIs, numbers for internal services, balance based on scenarios

2

Unified Naming Convention

Keep frontend and backend consistent, use tools for automatic sync, avoid case issues

3

Strict Input Validation

Always validate external input, use Schema tools, provide clear error messages

Remember: Readability first (use strings), Type safety (leverage tools), Backward compatibility (change cautiously). The right serialization strategy makes APIs more stable, debugging easier, and team collaboration smoother.