Back to all posts

RTTS: Reading and Building ABAP Types at Runtime

André Schärpf

RTTS stands for Runtime Type Services. It's ABAP's reflection system: a set of classes that can describe the type of any variable, structure, table, or object at runtime, and create new types on the fly. The name doesn't come up much in daily conversation, but the classes behind it (CL_ABAP_TYPEDESCR, CL_ABAP_STRUCTDESCR, CL_ABAP_TABLEDESCR) show up in almost every ABAP system that does anything dynamic.

RTTS splits into two halves:

  • RTTI (Runtime Type Identification) reads type information. Given a variable, a type name, or an object reference, it returns a descriptor object that tells you everything about that type.
  • RTTC (Runtime Type Creation) builds types. You define a structure from component definitions, create a table type with a specific key, or request an elementary type with a given length.

Both sides share the same class hierarchy. The split isn't in the classes themselves but in what the methods do: some describe, some construct. RTTI has been around since Release 4.6A, when the type description classes were first introduced alongside ABAP Objects. RTTC was added in Release 6.40, and both were unified under the RTTS umbrella at that point.

The class hierarchy

The hierarchy mirrors the ABAP type system. Each class represents a category of types, not an individual type.

CL_ABAP_TYPEDESCR                     RTTS root
├── CL_ABAP_DATADESCR                 data type descriptors
│   ├── CL_ABAP_ELEMDESCR             elementary types
│   │   └── CL_ABAP_ENUMDESCR         enumerations
│   ├── CL_ABAP_REFDESCR              reference types
│   └── CL_ABAP_COMPLEXDESCR          complex types
│       ├── CL_ABAP_STRUCTDESCR       structures
│       └── CL_ABAP_TABLEDESCR        table types
└── CL_ABAP_OBJECTDESCR               object type descriptors
    ├── CL_ABAP_CLASSDESCR            classes
    └── CL_ABAP_INTFDESCR             interfaces

In practice, you usually start at the top with a static method on CL_ABAP_TYPEDESCR and then downcast the returned descriptor to the concrete class you actually need, such as CL_ABAP_STRUCTDESCR or CL_ABAP_TABLEDESCR. The middle layers (DATADESCR, COMPLEXDESCR, OBJECTDESCR) mostly matter as organizing points in the API: they explain why certain methods belong to all data types, only to complex types, or only to class and interface descriptors.

CL_ABAP_ENUMDESCR was added in ABAP 7.51 when enumerations were introduced to the language. It inherits from CL_ABAP_ELEMDESCR, which makes sense: an enum is an elementary type with a constrained value set and an underlying base type.

All classes live in package SABP_RTTI.

RTTI: Inspecting types

RTTI starts with four static methods on CL_ABAP_TYPEDESCR:

MethodInputWhen to use
DESCRIBE_BY_DATA( )A data variableYou have a variable and want to know its type
DESCRIBE_BY_NAME( )A type name as stringYou have a DDIC type name or any type name
DESCRIBE_BY_DATA_REF( )A REF TO dataYou have a generic data reference
DESCRIBE_BY_OBJECT_REF( )A REF TO objectYou have an object reference

All four return a reference typed as CL_ABAP_TYPEDESCR. The actual runtime type of the returned object is the specific subclass that matches the described type. For an integer you get a CL_ABAP_ELEMDESCR, for a structure a CL_ABAP_STRUCTDESCR, and so on.

To access type-specific information, you downcast:

DATA(lo_type) = cl_abap_typedescr=>describe_by_data( some_variable ).

CASE lo_type->kind.
  WHEN cl_abap_typedescr=>kind_elem.
    DATA(lo_elem) = CAST cl_abap_elemdescr( lo_type ).

  WHEN cl_abap_typedescr=>kind_struct.
    DATA(lo_struct) = CAST cl_abap_structdescr( lo_type ).
    DATA(lt_components) = lo_struct->get_components( ).

  WHEN cl_abap_typedescr=>kind_table.
    DATA(lo_table) = CAST cl_abap_tabledescr( lo_type ).
    DATA(lo_line_type) = lo_table->get_table_line_type( ).
ENDCASE.

The KIND attribute tells you the broad category. The TYPE_KIND attribute gives you the specific ABAP type identifier: C for character, I for integer, g for string, h for table, and so on. The root class defines constants for all of these (TYPEKIND_CHAR, TYPEKIND_INT, TYPEKIND_STRING, TYPEKIND_TABLE).

Looking up a DDIC structure by name and iterating its components:

DATA(lo_sflight) = CAST cl_abap_structdescr(
  cl_abap_typedescr=>describe_by_name( 'SFLIGHT' ) ).

LOOP AT lo_sflight->get_components( ) INTO DATA(ls_comp).
  " ls_comp-name holds the component name: MANDT, CARRID, CONNID, ...
  " ls_comp-type holds a reference to that component's type descriptor
ENDLOOP.

For object references, the pattern works the same way. DESCRIBE_BY_OBJECT_REF returns a CL_ABAP_CLASSDESCR for the object's actual class, giving you access to its methods, attributes, events, and implemented interfaces:

DATA(lo_class) = CAST cl_abap_classdescr(
  cl_abap_typedescr=>describe_by_object_ref( lo_some_object ) ).

LOOP AT lo_class->methods INTO DATA(ls_method).
  " ls_method-name, ls_method-visibility, ls_method-is_abstract, ...
ENDLOOP.

RTTC: Creating types at runtime

RTTC uses static GET factory methods on the descriptor classes. Each leaf class for data types has factory methods that return a type descriptor for a type that may not exist as a named definition anywhere in the system.

For elementary types, CL_ABAP_ELEMDESCR has a GET_* method for each built-in type:

cl_abap_elemdescr=>get_c( 10 ).       " CHAR10
cl_abap_elemdescr=>get_n( 4 ).        " NUMC4
cl_abap_elemdescr=>get_d( ).          " DATS
cl_abap_elemdescr=>get_i( ).          " INT4
cl_abap_elemdescr=>get_string( ).     " STRING
cl_abap_elemdescr=>get_p( p_length = 8 p_decimals = 2 ). " packed
cl_abap_elemdescr=>get_decfloat34( ). " DECFLOAT34

For structures, you pass a component table where each entry has a name and a type descriptor:

DATA(lo_struct_type) = cl_abap_structdescr=>get(
  VALUE abap_component_tab(
    ( name = 'CARRID'  type = cl_abap_elemdescr=>get_c( 3 ) )
    ( name = 'CONNID'  type = cl_abap_elemdescr=>get_n( 4 ) )
    ( name = 'FLDATE'  type = cl_abap_elemdescr=>get_d( ) )
    ( name = 'PRICE'   type = cl_abap_elemdescr=>get_p( p_length = 8 p_decimals = 2 ) )
  ) ).

For tables, you wrap a line type with table properties:

DATA(lo_table_type) = cl_abap_tabledescr=>get(
  p_line_type  = lo_struct_type
  p_table_kind = cl_abap_tabledescr=>tablekind_std ).

RTTI and RTTC work together naturally. You can describe an existing DDIC type and use it as a building block for a new one:

DATA(lo_scarr) = CAST cl_abap_structdescr(
  cl_abap_typedescr=>describe_by_name( 'SCARR' ) ).

DATA(lo_extended) = cl_abap_structdescr=>get(
  VALUE abap_component_tab(
    ( name = 'ORIGINAL' type = lo_scarr )
    ( name = 'RATING'   type = cl_abap_elemdescr=>get_i( ) )
  ) ).

RTTS reuses descriptor objects. For a given type, the runtime keeps a single description object, so repeated GET calls for the same definition return the same descriptor reference instead of building duplicates.

CREATE DATA TYPE HANDLE

A type descriptor on its own is metadata. To allocate an actual data object from it, you use the TYPE HANDLE addition of the CREATE DATA statement:

DATA dref TYPE REF TO data.
CREATE DATA dref TYPE HANDLE lo_table_type.

This creates an anonymous data object on the heap whose type matches the descriptor. Since the variable is a generic REF TO data, you access the actual data through dereferencing:

FIELD-SYMBOLS <data> TYPE any.
ASSIGN dref->* TO <data>.

Using TYPE any is the most generic option and works regardless of what the descriptor represents. When you know the descriptor is a table type, you can narrow the field symbol to TYPE ANY TABLE to unlock table operations like LOOP AT and INSERT INTO TABLE. The full example below shows this in context: it builds a structure type, wraps it in a table type, allocates both, and populates the table using ASSIGN COMPONENT for dynamic field access:

DATA(lo_line) = cl_abap_structdescr=>get(
  VALUE abap_component_tab(
    ( name = 'NAME'   type = cl_abap_elemdescr=>get_string( ) )
    ( name = 'AGE'    type = cl_abap_elemdescr=>get_i( ) )
    ( name = 'ACTIVE' type = cl_abap_elemdescr=>get_c( 1 ) )
  ) ).

DATA(lo_table) = cl_abap_tabledescr=>get( lo_line ).

DATA dref TYPE REF TO data.
FIELD-SYMBOLS <persons> TYPE ANY TABLE.
CREATE DATA dref TYPE HANDLE lo_table.
ASSIGN dref->* TO <persons>.

DATA line_ref TYPE REF TO data.
FIELD-SYMBOLS <person> TYPE any.
CREATE DATA line_ref TYPE HANDLE lo_line.
ASSIGN line_ref->* TO <person>.

ASSIGN COMPONENT 'NAME' OF STRUCTURE <person> TO FIELD-SYMBOL(<name>).
ASSIGN COMPONENT 'AGE' OF STRUCTURE <person> TO FIELD-SYMBOL(<age>).
ASSIGN COMPONENT 'ACTIVE' OF STRUCTURE <person> TO FIELD-SYMBOL(<active>).

<name>   = 'Alice'.
<age>    = 30.
<active> = 'X'.
INSERT <person> INTO TABLE <persons>.

TYPE HANDLE only works with data type descriptors (subclasses of CL_ABAP_DATADESCR). You can't pass a CL_ABAP_CLASSDESCR or CL_ABAP_INTFDESCR here. If the descriptor is invalid, the runtime raises CX_SY_CREATE_DATA_ERROR.

Working with generic tables

When you assign a dynamically created table to a field symbol typed TYPE ANY TABLE, the table is fully functional at runtime, but your source-code access is generic. Operations that work on any table are fine: LOOP AT <table> ASSIGNING ..., INSERT ... INTO TABLE <table>, DESCRIBE TABLE, lines( ). What does not work is anything that requires the line type to be statically known. LOOP AT <table> INTO DATA(ls_line) fails because inline declarations need a concrete type. Direct component access like <line>-name also fails. For component-level access on a generic table, you need ASSIGN COMPONENT (as shown in the example above) or a cast to a statically known type.

One useful exception is whole-row key access using the pseudo component table_line. It refers to the entire row rather than named components, so it works even when the line type is not statically known. This is especially handy for tables with elementary line types, but it also works for full-row lookups on structured tables. It does not make the line type statically known. It just gives you one more operation that remains available on a generic table.

DATA(lo_string_table) = cl_abap_tabledescr=>get(
  p_line_type = cl_abap_elemdescr=>get_string( ) ).

DATA dref TYPE REF TO data.
CREATE DATA dref TYPE HANDLE lo_string_table.

FIELD-SYMBOLS <names> TYPE ANY TABLE.
ASSIGN dref->* TO <names>.

INSERT `Alice` INTO TABLE <names>.
INSERT `Bob` INTO TABLE <names>.

IF line_exists( <names>[ table_line = `Alice` ] ).
  " found
ENDIF.

What each class adds

The root class CL_ABAP_TYPEDESCR gives you ABSOLUTE_NAME, TYPE_KIND, KIND, LENGTH, and DECIMALS. Every type descriptor has these. The subclasses add what's specific to their type category.

CL_ABAP_ELEMDESCR adds OUTPUT_LENGTH and DDIC methods like GET_DDIC_FIELD() for data element metadata and GET_DDIC_FIXED_VALUES() for domain values. Useful when you need field labels or allowed values at runtime.

CL_ABAP_STRUCTDESCR has a COMPONENTS attribute (a flat list of component names and type codes) and a GET_COMPONENTS() method (a richer list where each entry includes a reference to the component's full type descriptor). STRUCT_KIND distinguishes flat from deep structures. GET_INCLUDED_VIEW() resolves INCLUDEs into a flat component list.

CL_ABAP_TABLEDESCR describes table types. It exposes TABLE_KIND (standard, sorted, hashed), KEY, HAS_UNIQUE_KEY, and GET_TABLE_LINE_TYPE() to navigate to the row type's descriptor. GET_KEYS() returns both primary and secondary keys if the table type defines them.

CL_ABAP_REFDESCR adds GET_REFERENCED_TYPE() to navigate from a reference type to whatever it points at.

CL_ABAP_OBJECTDESCR and its subclasses take a different angle. Instead of data layout, they describe class and interface metadata: METHODS, ATTRIBUTES, EVENTS, INTERFACES. You can navigate to a method parameter's type with GET_METHOD_PARAMETER_TYPE() or walk the inheritance chain with CL_ABAP_CLASSDESCR=>GET_SUPER_CLASS_TYPE().

CL_ABAP_ENUMDESCR exposes a MEMBERS attribute (a table of constant names and their values) and BASE_TYPE_KIND to tell you the underlying type of the enumeration.

These are the highlights. Each class has more public methods and constants worth exploring. Check the class definition in your system for the full surface.

ABAP Cloud

ABAP Cloud does support RTTS. SAP's ABAP Cloud overview explicitly lists runtime type information as part of the ABAP language library, and ABAP Cloud development in general is built around a cloud-optimized language version plus released public APIs. In other words, both RTTI and RTTC are part of the supported toolbox, not a loophole.

That covers the core RTTI entry points (DESCRIBE_BY_DATA, DESCRIBE_BY_DATA_REF, DESCRIBE_BY_OBJECT_REF, DESCRIBE_BY_NAME), the RTTC factory methods on the descriptor classes, and CREATE DATA ... TYPE HANDLE. Reflecting runtime values, building transient structures or table types, and allocating anonymous data objects from those descriptors all fit the ABAP Cloud model.

The real boundary is the usual ABAP Cloud one: released versus unreleased artifacts. Pure type reflection is fine. The moment you use RTTS to reach into classic repository metadata or to describe named objects outside the released API surface, the normal release-contract rules apply. So the short version is simple: RTTS is available in ABAP Cloud; the limits come from ABAP Cloud's release checks around what you reflect on, not from TYPE HANDLE or dynamic type creation themselves.