Automating Constant Class Generation from Domain Values in ABAP
Automating Constant Class Generation from Domain Values in ABAP
Hardcoding domain values in ABAP code is a recurring headache. It's fragile, hard to maintain, and dangerous in the long run. If a domain value changes, good luck hunting down every spot in your system where that value was manually entered. That’s why encapsulating domain values in a dedicated constants class is one of the solutions — it makes the code more reliable and readable.
But there's a catch: writing a class manually every time for every domain is tedious and time-consuming. So, I built a helper report that automates this process.

What It Does
The report, ZCC_GENERATE_CLASS, generates an ABAP class with constants based on the fixed values of a domain. You give it a data element, and it takes care of the rest—class name, constant definitions, even data types.
You can find the full implementation on GitHub:
https://github.com/JuliaBerteneva/ConstansClassFromDomain
How It Works
The selection screen of the report includes three input fields:
1. Data ElementYou enter the data element whose domain should be used to generate constants. The reason we ask for a data element and not a domain directly is simple: constants in the generated class need a specific data type, and the data element gives us both the type and the domain.
2. New Class Naming RuleThis is a pattern used to define the name of the class. You can include [D] in the rule, which will be replaced with the actual domain name during generation. This way, class names stay predictable and consistent.
3. Regenerate Existing Class (Checkbox)By default, the report checks if a class with the same name already exists, to avoid accidental overwrites. But if you've updated the domain (e.g., added or removed values) and want to regenerate the class, check this box to allow overwriting.
Where the Class Goes
The report tries to match the transport request and package based on the domain used. The assumption is that class generation typically happens right after domain creation or update, so tying them together in the same transport makes sense.
Under the Hood
Class generation uses the function module SEO_CLASS_CREATE_COMPLETE.
To fetch metadata from standard SAP tables (domains, data elements, packages, transport layers), I created a set of CDS views. These views simplify and streamline the report logic.
Here are the CDS views used in the report:
zcc_i_data_element_domain - receive domain name for data element
@AbapCatalog.sqlViewName: 'ZCCIDEDOM'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Domain for data element'
@Metadata.ignorePropagatedAnnotations: true
@VDM.viewType: #BASIC
define view zcc_i_data_element_domain 
    as select from dd04l
{
    key rollname,
    domname
}
zcc_i_domain_package - get package where domain is located
@AbapCatalog.sqlViewName: 'ZCCIDOMPACK'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Domain package'
@Metadata.ignorePropagatedAnnotations: true
@VDM.viewType: #BASIC
define view zcc_i_domain_package
  as select from tadir
{
  key obj_name,
  devclass
}
where
  object = 'DOMA'
zcc_i_domain_transport - get transport where domain is located
@AbapCatalog.sqlViewName: 'ZCCIDOMATRANS'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Data Element current transport'
@Metadata.ignorePropagatedAnnotations: true
@VDM.viewType: #BASIC
define view zcc_i_domain_transport as select from e071
{
    key obj_name,
    trkorr
}
where object = 'DOMA'
zcc_i_domain_values - get list of domain values
@AbapCatalog.sqlViewName: 'ZCCIDOMVAL'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Domain values'
@VDM.viewType: #BASIC
define view zcc_i_domain_values
  with parameters
    i_domname : domname
  as select from dd07l as l
    inner join   dd07t as t on  t.domname  = l.domname
                            and t.as4local = l.as4local
                            and t.valpos   = l.valpos
                            and t.as4vers  = l.as4vers
{
  key l.domvalue_l as Value,
      @Semantics.text: true
      t.ddtext     as Description
}
where
      l.domname    = :i_domname
  and t.ddlanguage = $session.system_language
Full Report Code
REPORT zcc_generate_class.

"Input parameter p_rollna is Data Element name. Program checks if there are values inside domain
"(under chosen data element) and generates a class with the values as constants.
"p_rule is a rule for new class naming. It should starts with zcl, [D] - means domain, this part will be replaced
"with the full name of data element. CC means constants class in default value.
"Parameter p_regen needed for cases when domain was changed after class generation. If this flag is set up
"while running the report existing class will be regenerated (new values for example will be added to class)

CONSTANTS: lc_default_rule TYPE tex50 VALUE 'ZCL_CC_[D]'.

PARAMETERS: p_rollna TYPE rollname OBLIGATORY,
            p_rule   TYPE text50 DEFAULT lc_default_rule,
            p_regen  AS CHECKBOX DEFAULT 'X'.

DATA: ls_properties TYPE vseoclass,
      lt_attributes TYPE seoo_attributes_r.

START-OF-SELECTION.
  IF p_rule IS INITIAL OR p_rule(3) <> 'ZCL'.
    p_rule = lc_default_rule.
  ENDIF.

  "get domain for mentioned data element
  SELECT SINGLE domname
    FROM zcc_i_data_element_domain
    INTO @DATA(lv_domname)
    WHERE rollname = @p_rollna.
  IF sy-subrc IS NOT INITIAL.
    MESSAGE e004(zcc_const_class).
  ENDIF.

  DATA(lv_classname) = p_rule.
  REPLACE ALL OCCURRENCES OF '[D]' IN lv_classname WITH lv_domname.

  IF p_regen IS INITIAL.
    "check if class doesn't exist
    SELECT COUNT( * )
      FROM seoclass
      WHERE clsname = @lv_classname.
    IF sy-dbcnt IS NOT INITIAL.
      MESSAGE e007(zcc_const_class).
    ENDIF.
  ENDIF.

  "get domain values
  SELECT *
    FROM zcc_i_domain_values( i_domname = @lv_domname )
    INTO TABLE @DATA(lt_domain_values).
  IF sy-subrc IS NOT INITIAL.
    MESSAGE e001(zcc_const_class).
    RETURN.
  ENDIF.

  "get package
  SELECT SINGLE devclass FROM zcc_i_domain_package
    INTO @DATA(lv_domain_package)
    WHERE obj_name = @lv_domname.
  IF sy-subrc IS NOT INITIAL.
    MESSAGE e002(zcc_const_class).
  ENDIF.

  "get transport request
  SELECT SINGLE trkorr FROM zcc_i_domain_transport
    INTO @DATA(lv_transport)
    WHERE obj_name = @lv_domname.
  IF sy-subrc IS NOT INITIAL.
    MESSAGE e005(zcc_const_class).
  ENDIF.

  ls_properties = VALUE #( descript = |Constants from { lv_domname } domain|
                           clsname = lv_classname
                           author = sy-uname
                           clsfinal = abap_true
                           exposure = seoc_exposure_public ).
  LOOP AT lt_domain_values ASSIGNING FIELD-SYMBOL(<ls_domain_values>).
    lt_attributes = VALUE #( BASE lt_attributes ( clsname = lv_classname
                                                  cmpname = condense( val = |GC_{ <ls_domain_values>-Value }| to = `` )
                                                  descript = <ls_domain_values>-Description
                                                  typtype = 1 "type
                                                  type = p_rollna " data element
                                                  attvalue = |`{ <ls_domain_values>-Value }`|
                                                  exposure = seoc_exposure_public
                                                  state = 1 "implemented
                                                  attdecltyp = 2 ) "constant
                           ).
  ENDLOOP.
  CALL FUNCTION 'SEO_CLASS_CREATE_COMPLETE'
    EXPORTING
      corrnr          = lv_transport
      devclass        = lv_domain_package
      overwrite       = abap_true
      version         = seoc_version_active
      suppress_dialog = abap_true
    CHANGING
      class           = ls_properties
      attributes      = lt_attributes
    EXCEPTIONS
      existing        = 1
      is_interface    = 2
      db_error        = 3
      component_error = 4
      no_access       = 5
      other           = 6
      OTHERS          = 7.
  IF sy-subrc IS NOT INITIAL.
    MESSAGE e003(zcc_const_class).
  ELSE.
    MESSAGE s006(zcc_const_class) WITH lv_classname lv_domain_package lv_transport.
  ENDIF.
Conclusion
This tool eliminates boilerplate and risk when working with domain values. Instead of manually syncing constants across your codebase, you get a clean, type-safe class generated in seconds. No more hardcoded magic strings. Just maintain the domain, run the report, and you're done.
Give it a try and feel free to contribute or suggest improvements on GitHub.
Made on
Tilda