diff --git a/.gitignore b/.gitignore
index 4775f6521..7bb06e392 100644
--- a/.gitignore
+++ b/.gitignore
@@ -82,6 +82,9 @@ src/durability/distributed/state_delta.capnp
 src/durability/distributed/state_delta.hpp
 src/durability/single_node/state_delta.hpp
 src/durability/single_node_ha/state_delta.hpp
+src/query/frontend/semantic/symbol.hpp
+src/query/frontend/semantic/symbol_serialization.capnp
+src/query/frontend/semantic/symbol_serialization.hpp
 src/query/plan/distributed_ops.capnp
 src/query/plan/distributed_ops.hpp
 src/query/plan/operator.hpp
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 8fbb73d71..d9f3a2990 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -56,6 +56,7 @@ define_add_lcp(add_lcp_single_node mg_single_node_sources generated_lcp_single_n
 
 add_lcp_single_node(durability/single_node/state_delta.lcp)
 add_lcp_single_node(query/frontend/ast/ast.lcp)
+add_lcp_single_node(query/frontend/semantic/symbol.lcp)
 add_lcp_single_node(query/plan/operator.lcp)
 
 add_custom_target(generate_lcp_single_node DEPENDS ${generated_lcp_single_node_files})
@@ -201,6 +202,9 @@ add_lcp_distributed(query/frontend/ast/ast.lcp)
 add_lcp_distributed(query/frontend/ast/ast_serialization.lcp CAPNP_SCHEMA @0xb107d3d6b4b1600b
         DEPENDS query/frontend/ast/ast.lcp)
 add_capnp_distributed(query/frontend/ast/ast_serialization.capnp)
+add_lcp_distributed(query/frontend/semantic/symbol.lcp)
+add_lcp_distributed(query/frontend/semantic/symbol_serialization.lcp CAPNP_SCHEMA @0x93c1dcee84e93b76
+        DEPENDS query/frontend/semantic/symbol.lcp)
 add_lcp_distributed(query/plan/operator.lcp)
 add_lcp_distributed(query/plan/distributed_ops.lcp CAPNP_SCHEMA @0xe5cae8d045d30c42
         DEPENDS query/plan/operator.lcp)
@@ -218,7 +222,7 @@ add_custom_target(generate_lcp_distributed DEPENDS ${generated_lcp_distributed_f
 
 add_capnp_distributed(communication/rpc/messages.capnp)
 add_capnp_distributed(durability/distributed/serialization.capnp)
-add_capnp_distributed(query/frontend/semantic/symbol.capnp)
+add_capnp_distributed(query/frontend/semantic/symbol_serialization.capnp)
 add_capnp_distributed(query/serialization.capnp)
 add_capnp_distributed(storage/distributed/rpc/serialization.capnp)
 
@@ -292,6 +296,7 @@ add_lcp_single_node_ha(database/single_node_ha/serialization.lcp CAPNP_SCHEMA @0
         DEPENDS durability/single_node_ha/state_delta.lcp)
 add_capnp_single_node_ha(database/single_node_ha/serialization.capnp)
 add_lcp_single_node_ha(query/frontend/ast/ast.lcp)
+add_lcp_single_node_ha(query/frontend/semantic/symbol.lcp)
 add_lcp_single_node_ha(query/plan/operator.lcp)
 add_lcp_single_node_ha(raft/raft_rpc_messages.lcp CAPNP_SCHEMA @0xa6c29b4287233b66)
 add_capnp_single_node_ha(raft/raft_rpc_messages.capnp)
diff --git a/src/distributed/bfs_rpc_messages.lcp b/src/distributed/bfs_rpc_messages.lcp
index 86ea15436..e61af48a2 100644
--- a/src/distributed/bfs_rpc_messages.lcp
+++ b/src/distributed/bfs_rpc_messages.lcp
@@ -22,7 +22,6 @@ cpp<#
 (lcp:capnp-import 'dist-ops "/query/plan/distributed_ops.capnp")
 (lcp:capnp-import 'query "/query/serialization.capnp")
 (lcp:capnp-import 'storage "/storage/distributed/rpc/serialization.capnp")
-(lcp:capnp-import 'symbol "/query/frontend/semantic/symbol.capnp")
 (lcp:capnp-import 'utils "/utils/serialization.capnp")
 
 (lcp:capnp-type-conversion "storage::EdgeAddress" "Storage.Address")
@@ -57,8 +56,7 @@ cpp<#
                                     std::vector<int> loaded_ast_uids;
                                     Load(&${member}, ${reader}, ast_storage, &loaded_ast_uids);
                                     cpp<#))
-      (symbol-table "query::SymbolTable"
-                    :capnp-type "Symbol.SymbolTable")
+      (symbol-table "query::SymbolTable" :capnp-type "Query.SymbolTable")
       (evaluation-context "query::EvaluationContext"
                           :capnp-type "Query.EvaluationContext"
                           :capnp-save (lambda (builder member capnp-name)
diff --git a/src/distributed/plan_rpc_messages.lcp b/src/distributed/plan_rpc_messages.lcp
index 7e3ec8ae0..689c488ca 100644
--- a/src/distributed/plan_rpc_messages.lcp
+++ b/src/distributed/plan_rpc_messages.lcp
@@ -15,7 +15,7 @@ cpp<#
 
 (lcp:capnp-import 'utils "/utils/serialization.capnp")
 (lcp:capnp-import 'plan "/query/plan/distributed_ops.capnp")
-(lcp:capnp-import 'sem "/query/frontend/semantic/symbol.capnp")
+(lcp:capnp-import 'query "/query/serialization.capnp")
 
 (defun load-plan (reader member capnp-name)
   (declare (ignore capnp-name))
@@ -47,7 +47,7 @@ cpp<#
       (plan "std::shared_ptr<query::plan::LogicalOperator>"
             :capnp-type "Utils.SharedPtr(Plan.LogicalOperator)"
             :capnp-save #'save-plan :capnp-load #'load-plan)
-      (symbol-table "query::SymbolTable" :capnp-type "Sem.SymbolTable")
+      (symbol-table "query::SymbolTable" :capnp-type "Query.SymbolTable")
       (storage "query::AstStorage" :initarg nil :capnp-save :dont-save)))
   (:response ()))
 
diff --git a/src/distributed/pull_produce_rpc_messages.lcp b/src/distributed/pull_produce_rpc_messages.lcp
index ccea8554c..5f384444d 100644
--- a/src/distributed/pull_produce_rpc_messages.lcp
+++ b/src/distributed/pull_produce_rpc_messages.lcp
@@ -31,7 +31,7 @@ cpp<#
 
 (lcp:capnp-import 'storage "/storage/distributed/rpc/serialization.capnp")
 (lcp:capnp-import 'query "/query/serialization.capnp")
-(lcp:capnp-import 'sem "/query/frontend/semantic/symbol.capnp")
+(lcp:capnp-import 'sem "/query/frontend/semantic/symbol_serialization.capnp")
 (lcp:capnp-import 'utils "/utils/serialization.capnp")
 
 (lcp:capnp-type-conversion "tx::CommandId" "UInt32")
diff --git a/src/query/frontend/ast/ast.lcp b/src/query/frontend/ast/ast.lcp
index 51564b111..302974233 100644
--- a/src/query/frontend/ast/ast.lcp
+++ b/src/query/frontend/ast/ast.lcp
@@ -38,7 +38,7 @@ cpp<#
 (lcp:capnp-namespace "query")
 
 (lcp:capnp-import 'storage "/storage/distributed/rpc/serialization.capnp")
-(lcp:capnp-import 'symbol "/query/frontend/semantic/symbol.capnp")
+(lcp:capnp-import 'symbol "/query/frontend/semantic/symbol_serialization.capnp")
 (lcp:capnp-import 'utils "/utils/serialization.capnp")
 
 (lcp:capnp-type-conversion "PropertyValue" "Storage.PropertyValue")
diff --git a/src/query/frontend/semantic/symbol.capnp b/src/query/frontend/semantic/symbol.capnp
deleted file mode 100644
index 076ea08cb..000000000
--- a/src/query/frontend/semantic/symbol.capnp
+++ /dev/null
@@ -1,31 +0,0 @@
-@0x93c1dcee84e93b76;
-
-using Cxx = import "/capnp/c++.capnp";
-$Cxx.namespace("query::capnp");
-
-struct Symbol {
-  enum Type {
-    any @0;
-    vertex @1;
-    edge @2;
-    path @3;
-    number @4;
-    edgeList @5;
-  }
-
-  name @0 :Text;
-  position @1 :Int32;
-  type @2 :Type;
-  userDeclared @3 :Bool;
-  tokenPosition @4 :Int32;
-}
-
-struct SymbolTable {
-  position @0 :Int32;
-  table @1 :List(Entry);
-
-  struct Entry {
-    key @0 :Int32;
-    val @1 :Symbol;
-  }
-}
diff --git a/src/query/frontend/semantic/symbol.hpp b/src/query/frontend/semantic/symbol.hpp
deleted file mode 100644
index cf7557312..000000000
--- a/src/query/frontend/semantic/symbol.hpp
+++ /dev/null
@@ -1,64 +0,0 @@
-#pragma once
-
-#include <string>
-
-namespace query {
-
-class Symbol {
- public:
-  // This is similar to TypedValue::Type, but this has `Any` type.
-  // TODO: Make a better Type structure which can store a generic List.
-  enum class Type { Any, Vertex, Edge, Path, Number, EdgeList };
-
-  static std::string TypeToString(Type type) {
-    const char *enum_string[] = {"Any",  "Vertex", "Edge",
-                                 "Path", "Number", "EdgeList"};
-    return enum_string[static_cast<int>(type)];
-  }
-
-  Symbol() {}
-  Symbol(const std::string &name, int position, bool user_declared,
-         Type type = Type::Any, int token_position = -1)
-      : name_(name),
-        position_(position),
-        user_declared_(user_declared),
-        type_(type),
-        token_position_(token_position) {}
-
-  bool operator==(const Symbol &other) const {
-    return position_ == other.position_ && name_ == other.name_ &&
-           type_ == other.type_;
-  }
-  bool operator!=(const Symbol &other) const { return !operator==(other); }
-
-  // TODO: Remove these since members are public
-  const auto &name() const { return name_; }
-  int position() const { return position_; }
-  Type type() const { return type_; }
-  bool user_declared() const { return user_declared_; }
-  int token_position() const { return token_position_; }
-
-  std::string name_;
-  int position_;
-  bool user_declared_ = true;
-  Type type_ = Type::Any;
-  int token_position_ = -1;
-};
-
-}  // namespace query
-
-namespace std {
-
-template <>
-struct hash<query::Symbol> {
-  size_t operator()(const query::Symbol &symbol) const {
-    size_t prime = 265443599u;
-    size_t hash = std::hash<int>{}(symbol.position());
-    hash ^= prime * std::hash<std::string>{}(symbol.name());
-    hash ^= prime * std::hash<bool>{}(symbol.user_declared());
-    hash ^= prime * std::hash<int>{}(static_cast<int>(symbol.type()));
-    return hash;
-  }
-};
-
-}  // namespace std
diff --git a/src/query/frontend/semantic/symbol.lcp b/src/query/frontend/semantic/symbol.lcp
new file mode 100644
index 000000000..f60e58dc5
--- /dev/null
+++ b/src/query/frontend/semantic/symbol.lcp
@@ -0,0 +1,77 @@
+#>cpp
+#pragma once
+
+#include <string>
+
+#include "utils/typeinfo.hpp"
+cpp<#
+
+(lcp:namespace query)
+
+(lcp:capnp-namespace "query")
+
+(lcp:define-class symbol ()
+  ((name "std::string" :scope :public)
+   (position :int64_t :scope :public)
+   (user-declared :bool :initval "true" :scope :public)
+   (type "Type" :initval "Type::ANY" :scope :public)
+   (token-position :int64_t :initval "-1" :scope :public))
+  (:public
+    ;; This is similar to TypedValue::Type, but this has `Any` type.
+    ;; TODO: Make a better Type structure which can store a generic List.
+    (lcp:define-enum type (any vertex edge path number edge-list)
+      (:serialize :capnp))
+    #>cpp
+    // TODO: Generate enum to string conversion from LCP. Note, that this is
+    // displayed to the end user, so we may want to have a pretty name of each
+    // value.
+    static std::string TypeToString(Type type) {
+      const char *enum_string[] = {"Any",  "Vertex", "Edge",
+                                   "Path", "Number", "EdgeList"};
+      return enum_string[static_cast<int>(type)];
+    }
+
+    Symbol() {}
+    Symbol(const std::string &name, int position, bool user_declared,
+           Type type = Type::ANY, int token_position = -1)
+        : name_(name),
+          position_(position),
+          user_declared_(user_declared),
+          type_(type),
+          token_position_(token_position) {}
+
+    bool operator==(const Symbol &other) const {
+      return position_ == other.position_ && name_ == other.name_ &&
+             type_ == other.type_;
+    }
+    bool operator!=(const Symbol &other) const { return !operator==(other); }
+
+    // TODO: Remove these since members are public
+    const auto &name() const { return name_; }
+    int position() const { return position_; }
+    Type type() const { return type_; }
+    bool user_declared() const { return user_declared_; }
+    int token_position() const { return token_position_; }
+    cpp<#)
+  (:serialize :capnp))
+
+(lcp:pop-namespace) ;; query
+
+#>cpp
+namespace std {
+
+template <>
+struct hash<query::Symbol> {
+  size_t operator()(const query::Symbol &symbol) const {
+    size_t prime = 265443599u;
+    size_t hash = std::hash<int>{}(symbol.position());
+    hash ^= prime * std::hash<std::string>{}(symbol.name());
+    hash ^= prime * std::hash<bool>{}(symbol.user_declared());
+    hash ^= prime * std::hash<int>{}(static_cast<int>(symbol.type()));
+    return hash;
+  }
+};
+
+}  // namespace std
+
+cpp<#
diff --git a/src/query/frontend/semantic/symbol_generator.cpp b/src/query/frontend/semantic/symbol_generator.cpp
index b675b61ce..74eccaaf2 100644
--- a/src/query/frontend/semantic/symbol_generator.cpp
+++ b/src/query/frontend/semantic/symbol_generator.cpp
@@ -26,8 +26,8 @@ auto SymbolGenerator::GetOrCreateSymbol(const std::string &name,
   auto search = scope_.symbols.find(name);
   if (search != scope_.symbols.end()) {
     auto symbol = search->second;
-    // Unless we have `Any` type, check that types match.
-    if (type != Symbol::Type::Any && symbol.type() != Symbol::Type::Any &&
+    // Unless we have `ANY` type, check that types match.
+    if (type != Symbol::Type::ANY && symbol.type() != Symbol::Type::ANY &&
         type != symbol.type()) {
       throw TypeMismatchError(name, Symbol::TypeToString(symbol.type()),
                               Symbol::TypeToString(type));
@@ -79,7 +79,7 @@ void SymbolGenerator::VisitReturnBody(ReturnBody &body, Where *where) {
     }
     // An improvement would be to infer the type of the expression, so that the
     // new symbol would have a more specific type.
-    symbol_table_[*named_expr] = CreateSymbol(name, true, Symbol::Type::Any,
+    symbol_table_[*named_expr] = CreateSymbol(name, true, Symbol::Type::ANY,
                                               named_expr->token_position_);
   }
   scope_.in_order_by = true;
@@ -230,7 +230,7 @@ SymbolGenerator::ReturnType SymbolGenerator::Visit(Identifier &ident) {
     // If we are in the pattern, and outside of a node or an edge, the
     // identifier is the pattern name.
     symbol = GetOrCreateSymbol(ident.name_, ident.user_declared_,
-                               Symbol::Type::Path);
+                               Symbol::Type::PATH);
   } else if (scope_.in_pattern && scope_.in_pattern_atom_identifier) {
     //  Patterns used to create nodes and edges cannot redeclare already
     //  established bindings. Declaration only happens in single node
@@ -242,15 +242,15 @@ SymbolGenerator::ReturnType SymbolGenerator::Visit(Identifier &ident) {
         HasSymbol(ident.name_)) {
       throw RedeclareVariableError(ident.name_);
     }
-    auto type = Symbol::Type::Vertex;
+    auto type = Symbol::Type::VERTEX;
     if (scope_.visiting_edge) {
       // Edge referencing is not allowed (like in Neo4j):
       // `MATCH (n) - [r] -> (n) - [r] -> (n) RETURN r` is not allowed.
       if (HasSymbol(ident.name_)) {
         throw RedeclareVariableError(ident.name_);
       }
-      type = scope_.visiting_edge->IsVariable() ? Symbol::Type::EdgeList
-                                                : Symbol::Type::Edge;
+      type = scope_.visiting_edge->IsVariable() ? Symbol::Type::EDGE_LIST
+                                                : Symbol::Type::EDGE;
     }
     symbol = GetOrCreateSymbol(ident.name_, ident.user_declared_, type);
   } else if (scope_.in_pattern && !scope_.in_pattern_atom_identifier &&
@@ -304,7 +304,7 @@ bool SymbolGenerator::PreVisit(Aggregation &aggr) {
   auto aggr_name =
       Aggregation::OpToString(aggr.op_) + std::to_string(aggr.uid_);
   symbol_table_[aggr] =
-      symbol_table_.CreateSymbol(aggr_name, false, Symbol::Type::Number);
+      symbol_table_.CreateSymbol(aggr_name, false, Symbol::Type::NUMBER);
   scope_.in_aggregation = true;
   scope_.has_aggregation = true;
   return true;
@@ -437,10 +437,10 @@ bool SymbolGenerator::PreVisit(EdgeAtom &edge_atom) {
       // be used in the missing filter expression.
       const auto *inner_edge = edge_atom.filter_lambda_.inner_edge;
       symbol_table_[*inner_edge] = symbol_table_.CreateSymbol(
-          inner_edge->name_, inner_edge->user_declared_, Symbol::Type::Edge);
+          inner_edge->name_, inner_edge->user_declared_, Symbol::Type::EDGE);
       const auto *inner_node = edge_atom.filter_lambda_.inner_node;
       symbol_table_[*inner_node] = symbol_table_.CreateSymbol(
-          inner_node->name_, inner_node->user_declared_, Symbol::Type::Vertex);
+          inner_node->name_, inner_node->user_declared_, Symbol::Type::VERTEX);
     }
     if (edge_atom.weight_lambda_.expression) {
       VisitWithIdentifiers(edge_atom.weight_lambda_.expression,
@@ -458,7 +458,7 @@ bool SymbolGenerator::PreVisit(EdgeAtom &edge_atom) {
     }
     symbol_table_[*edge_atom.total_weight_] = GetOrCreateSymbol(
         edge_atom.total_weight_->name_, edge_atom.total_weight_->user_declared_,
-        Symbol::Type::Number);
+        Symbol::Type::NUMBER);
   }
   return false;
 }
diff --git a/src/query/frontend/semantic/symbol_generator.hpp b/src/query/frontend/semantic/symbol_generator.hpp
index b386e2a26..6932e09b1 100644
--- a/src/query/frontend/semantic/symbol_generator.hpp
+++ b/src/query/frontend/semantic/symbol_generator.hpp
@@ -114,13 +114,13 @@ class SymbolGenerator : public HierarchicalTreeVisitor {
   // Returns a freshly generated symbol. Previous mapping of the same name to a
   // different symbol is replaced with the new one.
   auto CreateSymbol(const std::string &name, bool user_declared,
-                    Symbol::Type type = Symbol::Type::Any,
+                    Symbol::Type type = Symbol::Type::ANY,
                     int token_position = -1);
 
   // Returns the symbol by name. If the mapping already exists, checks if the
   // types match. Otherwise, returns a new symbol.
   auto GetOrCreateSymbol(const std::string &name, bool user_declared,
-                         Symbol::Type type = Symbol::Type::Any);
+                         Symbol::Type type = Symbol::Type::ANY);
 
   void VisitReturnBody(ReturnBody &body, Where *where = nullptr);
 
diff --git a/src/query/frontend/semantic/symbol_serialization.lcp b/src/query/frontend/semantic/symbol_serialization.lcp
new file mode 100644
index 000000000..9442c0071
--- /dev/null
+++ b/src/query/frontend/semantic/symbol_serialization.lcp
@@ -0,0 +1,11 @@
+#>cpp
+#pragma once
+
+#include "query/frontend/semantic/symbol.hpp"
+#include "query/frontend/semantic/symbol_serialization.capnp.h"
+cpp<#
+
+;; Generate serialization code
+;; TODO: This should be merged with query/serialization
+(load "query/frontend/semantic/symbol.lcp")
+
diff --git a/src/query/frontend/semantic/symbol_table.hpp b/src/query/frontend/semantic/symbol_table.hpp
index 79e9459de..a08ad22b0 100644
--- a/src/query/frontend/semantic/symbol_table.hpp
+++ b/src/query/frontend/semantic/symbol_table.hpp
@@ -12,7 +12,7 @@ class SymbolTable final {
  public:
   SymbolTable() {}
   Symbol CreateSymbol(const std::string &name, bool user_declared,
-                      Symbol::Type type = Symbol::Type::Any,
+                      Symbol::Type type = Symbol::Type::ANY,
                       int token_position = -1) {
     int position = position_++;
     return Symbol(name, position, user_declared, type, token_position);
diff --git a/src/query/plan/distributed.cpp b/src/query/plan/distributed.cpp
index cd5315b88..fc2db5fe6 100644
--- a/src/query/plan/distributed.cpp
+++ b/src/query/plan/distributed.cpp
@@ -1279,7 +1279,7 @@ class DistributedPlanner : public HierarchicalLogicalOperatorVisitor {
                         std::to_string(worker_ident->uid_) + "<-" +
                         worker_sym.name();
       auto merge_sym = distributed_plan_.symbol_table.CreateSymbol(
-          merge_name, false, Symbol::Type::Number);
+          merge_name, false, Symbol::Type::NUMBER);
       return Aggregate::Element{worker_ident, nullptr, op, merge_sym};
     };
     // Aggregate uses associative operation(s), so split the work across master
@@ -1320,12 +1320,12 @@ class DistributedPlanner : public HierarchicalLogicalOperatorVisitor {
         //  * master: SUM(worker_sum) / toFloat(SUM(worker_count)) AS avg
         case Aggregation::Op::AVG: {
           auto worker_sum_sym = distributed_plan_.symbol_table.CreateSymbol(
-              aggr.output_sym.name() + "_SUM", false, Symbol::Type::Number);
+              aggr.output_sym.name() + "_SUM", false, Symbol::Type::NUMBER);
           Aggregate::Element worker_sum{aggr.value, aggr.key,
                                         Aggregation::Op::SUM, worker_sum_sym};
           worker_aggrs.emplace_back(worker_sum);
           auto worker_count_sym = distributed_plan_.symbol_table.CreateSymbol(
-              aggr.output_sym.name() + "_COUNT", false, Symbol::Type::Number);
+              aggr.output_sym.name() + "_COUNT", false, Symbol::Type::NUMBER);
           Aggregate::Element worker_count{
               aggr.value, aggr.key, Aggregation::Op::COUNT, worker_count_sym};
           worker_aggrs.emplace_back(worker_count);
diff --git a/src/query/plan/operator.lcp b/src/query/plan/operator.lcp
index e7da102f5..cef275265 100644
--- a/src/query/plan/operator.lcp
+++ b/src/query/plan/operator.lcp
@@ -126,7 +126,7 @@ cpp<#
 (lcp:capnp-import 'utils "/utils/serialization.capnp")
 (lcp:capnp-import 'storage "/storage/distributed/rpc/serialization.capnp")
 (lcp:capnp-import 'ast "/query/frontend/ast/ast_serialization.capnp")
-(lcp:capnp-import 'semantic "/query/frontend/semantic/symbol.capnp")
+(lcp:capnp-import 'semantic "/query/frontend/semantic/symbol_serialization.capnp")
 (lcp:capnp-import 'query "/query/serialization.capnp")
 
 (lcp:capnp-type-conversion "Symbol" "Semantic.Symbol")
diff --git a/src/query/serialization.capnp b/src/query/serialization.capnp
index 1757bf877..58c025336 100644
--- a/src/query/serialization.capnp
+++ b/src/query/serialization.capnp
@@ -2,6 +2,7 @@
 
 using Ast = import "/query/frontend/ast/ast_serialization.capnp";
 using Cxx = import "/capnp/c++.capnp";
+using Sem = import "/query/frontend/semantic/symbol_serialization.capnp";
 using Storage = import "/storage/distributed/rpc/serialization.capnp";
 using Utils = import "/utils/serialization.capnp";
 
@@ -45,3 +46,13 @@ struct TypedValue {
     edges @1 :List(Storage.EdgeAccessor);
   }
 }
+
+struct SymbolTable {
+  position @0 :Int32;
+  table @1 :List(Entry);
+
+  struct Entry {
+    key @0 :Int32;
+    val @1 :Sem.Symbol;
+  }
+}
diff --git a/src/query/serialization.hpp b/src/query/serialization.hpp
index 3297d68f6..948b5e955 100644
--- a/src/query/serialization.hpp
+++ b/src/query/serialization.hpp
@@ -2,8 +2,7 @@
 
 #include "query/common.hpp"
 #include "query/context.hpp"
-#include "query/frontend/semantic/symbol.capnp.h"
-#include "query/frontend/semantic/symbol.hpp"
+#include "query/frontend/semantic/symbol_serialization.hpp"
 #include "query/frontend/semantic/symbol_table.hpp"
 #include "query/serialization.capnp.h"
 #include "query/typed_value.hpp"
@@ -36,60 +35,6 @@ void Save(const TypedValueVectorCompare &comparator,
 void Load(TypedValueVectorCompare *comparator,
           const capnp::TypedValueVectorCompare::Reader &reader);
 
-inline void Save(const Symbol &symbol, capnp::Symbol::Builder *builder) {
-  builder->setName(symbol.name());
-  builder->setPosition(symbol.position());
-  builder->setUserDeclared(symbol.user_declared());
-  builder->setTokenPosition(symbol.token_position());
-  switch (symbol.type()) {
-    case Symbol::Type::Any:
-      builder->setType(capnp::Symbol::Type::ANY);
-      break;
-    case Symbol::Type::Edge:
-      builder->setType(capnp::Symbol::Type::EDGE);
-      break;
-    case Symbol::Type::EdgeList:
-      builder->setType(capnp::Symbol::Type::EDGE_LIST);
-      break;
-    case Symbol::Type::Number:
-      builder->setType(capnp::Symbol::Type::NUMBER);
-      break;
-    case Symbol::Type::Path:
-      builder->setType(capnp::Symbol::Type::PATH);
-      break;
-    case Symbol::Type::Vertex:
-      builder->setType(capnp::Symbol::Type::VERTEX);
-      break;
-  }
-}
-
-inline void Load(Symbol *symbol, const capnp::Symbol::Reader &reader) {
-  symbol->name_ = reader.getName();
-  symbol->position_ = reader.getPosition();
-  symbol->user_declared_ = reader.getUserDeclared();
-  symbol->token_position_ = reader.getTokenPosition();
-  switch (reader.getType()) {
-    case capnp::Symbol::Type::ANY:
-      symbol->type_ = Symbol::Type::Any;
-      break;
-    case capnp::Symbol::Type::EDGE:
-      symbol->type_ = Symbol::Type::Edge;
-      break;
-    case capnp::Symbol::Type::EDGE_LIST:
-      symbol->type_ = Symbol::Type::EdgeList;
-      break;
-    case capnp::Symbol::Type::NUMBER:
-      symbol->type_ = Symbol::Type::Number;
-      break;
-    case capnp::Symbol::Type::PATH:
-      symbol->type_ = Symbol::Type::Path;
-      break;
-    case capnp::Symbol::Type::VERTEX:
-      symbol->type_ = Symbol::Type::Vertex;
-      break;
-  }
-}
-
 inline void Save(const SymbolTable &symbol_table,
                  capnp::SymbolTable::Builder *builder) {
   builder->setPosition(symbol_table.max_position());
diff --git a/tests/benchmark/serialization.cpp b/tests/benchmark/serialization.cpp
index 6b65f688e..dc8de73e7 100644
--- a/tests/benchmark/serialization.cpp
+++ b/tests/benchmark/serialization.cpp
@@ -6,7 +6,7 @@
 #include <capnp/serialize.h>
 #include <kj/std/iostream.h>
 
-#include "query/frontend/semantic/symbol.capnp.h"
+#include "query/frontend/semantic/symbol_serialization.capnp.h"
 #include "query/frontend/semantic/symbol.hpp"
 
 #include "communication/rpc/serialization.hpp"
@@ -17,8 +17,8 @@ class SymbolVectorFixture : public benchmark::Fixture {
 
   void SetUp(const benchmark::State &state) override {
     using Type = ::query::Symbol::Type;
-    std::vector<Type> types{Type::Any,  Type::Vertex, Type::Edge,
-                            Type::Path, Type::Number, Type::EdgeList};
+    std::vector<Type> types{Type::ANY,  Type::VERTEX, Type::EDGE,
+                            Type::PATH, Type::NUMBER, Type::EDGE_LIST};
     symbols_.reserve(state.range(0));
     for (int i = 0; i < state.range(0); ++i) {
       std::string name = "Symbol " + std::to_string(i);
@@ -80,7 +80,7 @@ BENCHMARK_DEFINE_F(SymbolVectorFixture, CapnpDeserial)
     symbols.reserve(symbols_reader.size());
     for (const auto &sym : symbols_reader) {
       symbols.emplace_back(sym.getName().cStr(), sym.getPosition(),
-                           sym.getUserDeclared(), query::Symbol::Type::Any,
+                           sym.getUserDeclared(), query::Symbol::Type::ANY,
                            sym.getTokenPosition());
     }
   }
@@ -99,17 +99,17 @@ BENCHMARK_REGISTER_F(SymbolVectorFixture, CapnpDeserial)
 
 uint8_t Type2Int(query::Symbol::Type type) {
   switch (type) {
-    case query::Symbol::Type::Any:
+    case query::Symbol::Type::ANY:
       return 1;
-    case query::Symbol::Type::Vertex:
+    case query::Symbol::Type::VERTEX:
       return 2;
-    case query::Symbol::Type::Edge:
+    case query::Symbol::Type::EDGE:
       return 3;
-    case query::Symbol::Type::Path:
+    case query::Symbol::Type::PATH:
       return 4;
-    case query::Symbol::Type::Number:
+    case query::Symbol::Type::NUMBER:
       return 5;
-    case query::Symbol::Type::EdgeList:
+    case query::Symbol::Type::EDGE_LIST:
       return 6;
   }
 }
@@ -117,17 +117,17 @@ uint8_t Type2Int(query::Symbol::Type type) {
 query::Symbol::Type Int2Type(uint8_t value) {
   switch (value) {
     case 1:
-      return query::Symbol::Type::Any;
+      return query::Symbol::Type::ANY;
     case 2:
-      return query::Symbol::Type::Vertex;
+      return query::Symbol::Type::VERTEX;
     case 3:
-      return query::Symbol::Type::Edge;
+      return query::Symbol::Type::EDGE;
     case 4:
-      return query::Symbol::Type::Path;
+      return query::Symbol::Type::PATH;
     case 5:
-      return query::Symbol::Type::Number;
+      return query::Symbol::Type::NUMBER;
     case 6:
-      return query::Symbol::Type::EdgeList;
+      return query::Symbol::Type::EDGE_LIST;
   }
   CHECK(false);
 }
diff --git a/tests/unit/distributed_query_plan.cpp b/tests/unit/distributed_query_plan.cpp
index 74ab8b7fe..ead2f549e 100644
--- a/tests/unit/distributed_query_plan.cpp
+++ b/tests/unit/distributed_query_plan.cpp
@@ -1996,9 +1996,9 @@ TYPED_TEST(TestPlanner, DistributedOptionalScanExpandExisting) {
 
 TEST(CapnpSerial, Union) {
   std::vector<Symbol> left_symbols{
-      Symbol("symbol", 1, true, Symbol::Type::Edge)};
+      Symbol("symbol", 1, true, Symbol::Type::EDGE)};
   std::vector<Symbol> right_symbols{
-      Symbol("symbol", 3, true, Symbol::Type::Any)};
+      Symbol("symbol", 3, true, Symbol::Type::ANY)};
   auto union_symbols = right_symbols;
   auto union_op = std::make_unique<Union>(nullptr, nullptr, union_symbols,
                                           left_symbols, right_symbols);
@@ -2020,9 +2020,9 @@ TEST(CapnpSerial, Union) {
 
 TEST(CapnpSerial, Cartesian) {
   std::vector<Symbol> left_symbols{
-      Symbol("left_symbol", 1, true, Symbol::Type::Edge)};
+      Symbol("left_symbol", 1, true, Symbol::Type::EDGE)};
   std::vector<Symbol> right_symbols{
-      Symbol("right_symbol", 3, true, Symbol::Type::Any)};
+      Symbol("right_symbol", 3, true, Symbol::Type::ANY)};
   auto cartesian = std::make_unique<Cartesian>(nullptr, left_symbols, nullptr,
                                                right_symbols);
   std::unique_ptr<LogicalOperator> loaded_plan;
@@ -2057,7 +2057,7 @@ TEST(CapnpSerial, Synchronize) {
 }
 
 TEST(CapnpSerial, PullRemote) {
-  std::vector<Symbol> symbols{Symbol("symbol", 1, true, Symbol::Type::Edge)};
+  std::vector<Symbol> symbols{Symbol("symbol", 1, true, Symbol::Type::EDGE)};
   auto pull_remote = std::make_unique<PullRemote>(nullptr, 42, symbols);
   std::unique_ptr<LogicalOperator> loaded_plan;
   ::capnp::MallocMessageBuilder message;
@@ -2077,7 +2077,7 @@ TEST(CapnpSerial, PullRemoteOrderBy) {
   auto once = std::make_shared<Once>();
   AstStorage storage;
   std::vector<Symbol> symbols{
-      Symbol("my_symbol", 2, true, Symbol::Type::Vertex, 3)};
+      Symbol("my_symbol", 2, true, Symbol::Type::VERTEX, 3)};
   std::vector<query::SortItem> order_by{
       {query::Ordering::ASC, IDENT("my_symbol")}};
   auto pull_remote_order_by =
diff --git a/tests/unit/query_plan_match_filter_return.cpp b/tests/unit/query_plan_match_filter_return.cpp
index a534c1c46..b40bac54a 100644
--- a/tests/unit/query_plan_match_filter_return.cpp
+++ b/tests/unit/query_plan_match_filter_return.cpp
@@ -799,7 +799,7 @@ TEST_F(QueryPlanExpandVariable, NamedPath) {
   };
 
   auto path_symbol =
-      symbol_table.CreateSymbol("path", true, Symbol::Type::Path);
+      symbol_table.CreateSymbol("path", true, Symbol::Type::PATH);
   auto create_path = std::make_shared<ConstructNamedPath>(
       expand, path_symbol,
       std::vector<Symbol>{find_symbol("n"), e, find_symbol("m")});
diff --git a/tests/unit/query_semantic.cpp b/tests/unit/query_semantic.cpp
index 4aa482507..9e9e57a38 100644
--- a/tests/unit/query_semantic.cpp
+++ b/tests/unit/query_semantic.cpp
@@ -31,12 +31,12 @@ TEST_F(TestSymbolGenerator, MatchNodeReturn) {
   auto match = dynamic_cast<Match *>(query_ast->single_query_->clauses_[0]);
   auto pattern = match->patterns_[0];
   auto pattern_sym = symbol_table[*pattern->identifier_];
-  EXPECT_EQ(pattern_sym.type(), Symbol::Type::Path);
+  EXPECT_EQ(pattern_sym.type(), Symbol::Type::PATH);
   EXPECT_FALSE(pattern_sym.user_declared());
   auto node_atom = dynamic_cast<NodeAtom *>(pattern->atoms_[0]);
   auto node_sym = symbol_table[*node_atom->identifier_];
   EXPECT_EQ(node_sym.name(), "node_atom_1");
-  EXPECT_EQ(node_sym.type(), Symbol::Type::Vertex);
+  EXPECT_EQ(node_sym.type(), Symbol::Type::VERTEX);
   auto ret = dynamic_cast<Return *>(query_ast->single_query_->clauses_[1]);
   auto named_expr = ret->body_.named_expressions[0];
   auto column_sym = symbol_table[*named_expr];
@@ -56,7 +56,7 @@ TEST_F(TestSymbolGenerator, MatchNamedPattern) {
   auto match = dynamic_cast<Match *>(query_ast->single_query_->clauses_[0]);
   auto pattern = match->patterns_[0];
   auto pattern_sym = symbol_table[*pattern->identifier_];
-  EXPECT_EQ(pattern_sym.type(), Symbol::Type::Path);
+  EXPECT_EQ(pattern_sym.type(), Symbol::Type::PATH);
   EXPECT_EQ(pattern_sym.name(), "p");
   EXPECT_TRUE(pattern_sym.user_declared());
 }
@@ -95,7 +95,7 @@ TEST_F(TestSymbolGenerator, CreateNodeReturn) {
   auto node_atom = dynamic_cast<NodeAtom *>(pattern->atoms_[0]);
   auto node_sym = symbol_table[*node_atom->identifier_];
   EXPECT_EQ(node_sym.name(), "n");
-  EXPECT_EQ(node_sym.type(), Symbol::Type::Vertex);
+  EXPECT_EQ(node_sym.type(), Symbol::Type::VERTEX);
   auto ret = dynamic_cast<Return *>(query_ast->single_query_->clauses_[1]);
   auto named_expr = ret->body_.named_expressions[0];
   auto column_sym = symbol_table[*named_expr];
@@ -195,7 +195,7 @@ TEST_F(TestSymbolGenerator, CreateDelete) {
   EXPECT_EQ(symbol_table.max_position(), 2);
   auto node_symbol = symbol_table.at(*node->identifier_);
   auto ident_symbol = symbol_table.at(*ident);
-  EXPECT_EQ(node_symbol.type(), Symbol::Type::Vertex);
+  EXPECT_EQ(node_symbol.type(), Symbol::Type::VERTEX);
   EXPECT_EQ(node_symbol, ident_symbol);
 }
 
@@ -286,18 +286,18 @@ TEST_F(TestSymbolGenerator, CreateMultiExpand) {
   auto n1 = symbol_table.at(*node_n1->identifier_);
   auto n2 = symbol_table.at(*node_n2->identifier_);
   EXPECT_EQ(n1, n2);
-  EXPECT_EQ(n1.type(), Symbol::Type::Vertex);
+  EXPECT_EQ(n1.type(), Symbol::Type::VERTEX);
   auto m = symbol_table.at(*node_m->identifier_);
-  EXPECT_EQ(m.type(), Symbol::Type::Vertex);
+  EXPECT_EQ(m.type(), Symbol::Type::VERTEX);
   EXPECT_NE(m, n1);
   auto l = symbol_table.at(*node_l->identifier_);
-  EXPECT_EQ(l.type(), Symbol::Type::Vertex);
+  EXPECT_EQ(l.type(), Symbol::Type::VERTEX);
   EXPECT_NE(l, n1);
   EXPECT_NE(l, m);
   auto r = symbol_table.at(*edge_r->identifier_);
   auto p = symbol_table.at(*edge_p->identifier_);
-  EXPECT_EQ(r.type(), Symbol::Type::Edge);
-  EXPECT_EQ(p.type(), Symbol::Type::Edge);
+  EXPECT_EQ(r.type(), Symbol::Type::EDGE);
+  EXPECT_EQ(p.type(), Symbol::Type::EDGE);
   EXPECT_NE(r, p);
 }
 
@@ -408,11 +408,11 @@ TEST_F(TestSymbolGenerator, MatchWithCreate) {
   // symbols: pattern * 2, `n`, `m`, `r`
   EXPECT_EQ(symbol_table.max_position(), 5);
   auto n = symbol_table.at(*node_1->identifier_);
-  EXPECT_EQ(n.type(), Symbol::Type::Vertex);
+  EXPECT_EQ(n.type(), Symbol::Type::VERTEX);
   auto m = symbol_table.at(*node_2->identifier_);
   EXPECT_NE(n, m);
   // Currently we don't infer expression types, so we lost true type of 'm'.
-  EXPECT_EQ(m.type(), Symbol::Type::Any);
+  EXPECT_EQ(m.type(), Symbol::Type::ANY);
   EXPECT_EQ(m, symbol_table.at(*node_3->identifier_));
 }
 
@@ -703,7 +703,7 @@ TEST_F(TestSymbolGenerator, MatchVariablePathUsingIdentifier) {
   auto l = symbol_table.at(*node_l->identifier_);
   EXPECT_EQ(l, symbol_table.at(*l_prop->expression_));
   auto r = symbol_table.at(*edge->identifier_);
-  EXPECT_EQ(r.type(), Symbol::Type::EdgeList);
+  EXPECT_EQ(r.type(), Symbol::Type::EDGE_LIST);
 }
 
 TEST_F(TestSymbolGenerator, MatchVariablePathUsingUnboundIdentifier) {