Building a Reusable API Validation Class in ABAP

Building a Reusable API Validation Class in ABAP

Muhammad Hafizh Roihan
9 min read

Inspired by go-playground/validator

github.com/go-playground/validator →

1. The Problem: Unstructured API Validation in ABAP

Anyone who has worked with APIs in the SAP ecosystem — whether OData, REST via HTTP handler, or RFC exposed to external systems — has likely faced the same problem: scattered, inconsistent request validation.

Imagine a typical API handler. Inside, you find something like this:

" Manual validation scattered everywhere
IF lv_email IS INITIAL.
  lv_error = 'Email is required'.
  RETURN.
ENDIF.

IF lv_email NS '@'.
  lv_error = 'Email format is invalid'.
  RETURN.
ENDIF.

IF strlen( lv_username ) < 3.
  lv_error = 'Username too short'.
  RETURN.
ENDIF.

" ...and so on for every field

This pattern has several serious problems:

  • Not reusable — the same logic is rewritten in every handler
  • Hard to maintain — when validation rules change, you have to hunt them down across the entire codebase
  • Fail-fast only — the user only sees one error at a time, not all errors at once
  • No standard — every developer writes validation in their own style

2. Inspiration from Golang: go-playground/validator

In the Golang world, this problem has long been solved elegantly using the go-playground/validator library. Validation rules are declared directly on the struct definition via struct tags:

type AccountRegisterRequest struct {
    Email        string `json:"email" binding:"required,email"`
    BusinessName string `json:"businessName" binding:"required"`
    Name         string `json:"name" binding:"required"`
}

What makes this pattern powerful is its declarative nature: validation lives alongside the data itself. Anyone reading this struct immediately knows the rules without having to trace validation logic elsewhere.

ABAP does not have struct tags, of course. But the spirit can be adapted: rule-based, readable in a single line, and reusable across the entire codebase.

Golang vs ABAP validation comparison

3. The Solution: ZCL_API_VALIDATOR

The solution is a class called ZCL_API_VALIDATOR that adapts this philosophy into the ABAP paradigm. There are three main components to understand.

Before diving into the details, here is a high-level overview of how the class works:

ZCL_API_VALIDATOR high-level overview

3.1 Validator Registry

The heart of this class is a validator registry — an internal table that maps rule names to the Function Modules responsible for executing them. The registry is populated once when the class is first loaded:

" Registry populated in the class constructor
" Format: rule name → Function Module name
vlkey = 'required'  →  ZFM_VALIDATE_REQUIRED
vlkey = 'email'     →  ZFM_VALIDATE_EMAIL
vlkey = 'oneof'     →  ZFM_VALIDATE_ONEOF
vlkey = 'min'       →  ZFM_VALIDATE_MIN
vlkey = 'max'       →  ZFM_VALIDATE_MAX
" ...and so on

The advantage of this pattern is clear: to add a new validator, simply register the new rule and create its Function Module. No changes whatsoever to the core class logic. This is the Open/Closed Principle in practice within ABAP.

3.2 Expressive Syntax via MACRO

To make the class as easy and expressive to use as possible, an ABAP MACRO is used as a wrapper. The result is validation that can be written in a single readable line:

" One line = one field with its rules
m_validate 'email'    lv_email    'required email'          abap_false lt_req.
m_validate 'gender'   lv_gender   'oneof=MALE,FEMALE'       abap_false lt_req.
m_validate 'age'      lv_age      'required min=18 max=65'  abap_true  lt_req.
" Format: m_validate <field_name> <value> <rules> <is_numeric> <queue>

3.3 Input and Output Structures

The class uses two main data structures. The input structure holds information about the field to be validated, while the output structure stores the validation result:

" Input: field information to be validated
ZSTRU_FIELD_INPUT:
  FLDNM  TYPE STRING   " field name
  VALUE  TYPE STRING   " field value
  VLDTE  TYPE STRING   " rule string, e.g. 'required email'
  ISNUM  TYPE CHAR1    " flag: numeric comparison or string length?

" Output: validation result per field
ZSTRU_VALIDATION_RESULT:
  FLDNM  TYPE STRING   " field name
  MESSG  TYPE STRING   " error message or 'valid'
  VNRES  TYPE CHAR1    " 'S' = success, 'E' = error

4. How It Works Under the Hood

When the validation method is called, it processes the field queue one by one. Several key mechanisms make this work reliably.

4.1 Each Field Can Have Multiple Rules

A rule string like 'required email' is automatically split into two separate rules: 'required' and 'email'. Both are executed in sequence for the same field. This allows flexible rule combinations without writing any additional code.

4.2 Optional Fields Are Skipped When Empty

There is an important gate check before validation starts: if a field does not have the 'required' rule and its value is empty, the field is immediately considered valid and skipped. This prevents validators like 'email' or 'min' from returning errors on fields that are intentionally left blank.

" How the logic works:
" Field 'nickname' with rule 'min=3' and empty value
"   → not required, value empty → immediately valid, skip

" Field 'email' with rule 'required email' and empty value
"   → required rule present → enters validation → error: field required

4.3 Dynamic Dispatch to the Right Validator

Once a rule is found in the registry, its Function Module is called dynamically. There is no long IF-ELSEIF chain here — each validator is an independent unit called through the registry. This is exactly why adding a new validator requires no changes to the core logic.

5. The Three Public Methods

This class exposes three public methods that work in sequence. Understanding all three is the key to using the class effectively.

5.1 M_SET_VALIDATION — Register a Field to the Queue

This method registers a single field along with its validation rules into the queue. Each call adds one new entry — no validation is executed at this point.

" Parameters:
" I_FLDNM → field name (used in error messages)
" I_VALUE → field value to be validated
" I_VLDTE → rule string, e.g. 'required email'
" I_ISNUM → numeric flag (default: false)
" CT_FVLD → validation queue (appended to, not replaced)

" Direct call example:
lo_validator->m_set_validation(
  EXPORTING
    i_fldnm = 'email'
    i_value = lv_email
    i_vldte = 'required email'
    i_isnum = abap_false
  CHANGING
    ct_fvld = lt_field_queue
).

5.2 M_VALIDATE_FIELDS — Execute All Validations

This is the core of the class. It takes the entire field queue, runs all rules for every field, and returns a table of validation results.

" Parameters:
" IT_FLDREQ → the registered field queue
" RT_VLTRES → validation result table (one row per field)

lt_results = lo_validator->m_validate_fields( lt_field_queue ).

" Result structure per field:
" FLDNM → field name
" VNRES → 'S' (success) or 'E' (error)
" MESSG → 'valid' or error description

This method processes all fields at once and collects all errors — it does not stop at the first one. If three fields are invalid, the client receives all three error messages in a single response.

5.3 M_GET_ERROR_MESSAGE — Retrieve the Error String

A utility method that converts the validation result table into a single error string ready to send to the client. If all fields are valid, this method returns an empty string.

" Parameters:
" IT_RES  → result table from M_VALIDATE_FIELDS
" RET_MSG → combined string of all error messages

lv_errors = lo_validator->m_get_error_message( lt_results ).

" Example output when two errors exist:
" 'email is required, username must not contain special characters'

" If all valid → lv_errors is empty (IS INITIAL)

The usage pattern is always the same: check whether the result string is empty or not. If not empty, return an error to the client immediately with that message.

6. Real-World Usage Example

Here is a complete example of using this class inside an API handler for a user registration endpoint:

METHOD handle_user_registration.
  DATA: lo_validator   TYPE REF TO zcl_api_validator,
        lt_field_queue TYPE ztab_field_input,
        lt_results     TYPE ztab_validation_result,
        lv_errors      TYPE string.

  " Instantiate the validator
  CREATE OBJECT lo_validator.
  o_validator = lo_validator.

  " ── Register fields for validation ──
  m_validate 'username' lv_req-data-username 'required nospecialchar'  abap_false lt_field_queue.
  m_validate 'email'    lv_req-data-email    'required email'           abap_false lt_field_queue.
  m_validate 'age'      lv_req-data-age      'required min=18 max=60'   abap_true  lt_field_queue.
  m_validate 'gender'   lv_req-data-gender   'oneof=MALE,FEMALE'        abap_false lt_field_queue.

  " ── Run all validations at once ──
  lt_results = lo_validator->m_validate_fields( lt_field_queue ).

  " ── Check for errors ──
  lv_errors = lo_validator->m_get_error_message( lt_results ).

  IF lv_errors IS NOT INITIAL.
    " Return HTTP 400 with all error messages
    RETURN.
  ENDIF.

  " Continue to business logic...
ENDMETHOD.

Notice how multiple rules can be combined on a single field — for example, age is simultaneously required, at least 18, and at most 60. All validations are collected first, then executed at once, so the client receives all error messages in a single response.

Postman API validation example

7. Available Rules

Here are some of the available validators:

RuleExampleDescription
required'required'Field must not be empty
email'email'Value must be a valid email format
oneof'oneof=A,B,C'Value must be one of the specified options
min'min=8'Minimum value (numeric) or minimum string length
max'max=24'Maximum value (numeric) or maximum string length
len'len=10'String length must be exactly N characters
nospecialchar'nospecialchar'Value must not contain special characters

Rules can be combined in a single string separated by spaces:

" Combining multiple rules on one field
m_validate 'username' lv_username 'required nospecialchar'  abap_false lt_req.
m_validate 'bio'      lv_bio      'max=200'                 abap_false lt_req.
m_validate 'age'      lv_age      'required min=18 max=65'  abap_true  lt_req.

8. Adding a New Validator

One of the greatest strengths of this design is how easy it is to extend. Adding a new validator requires only two steps:

Step 1: Register the new rule in the registry

" Add one entry in the class constructor
APPEND VALUE zstru_validator_key(
  vlkey = 'mynewrule'
  fmnam = 'ZFM_VALIDATE_MYNEWRULE'
) TO validator_registry.

Step 2: Create a Function Module following the same contract

FUNCTION ZFM_VALIDATE_MYNEWRULE.
" Required interface:
"   IMPORTING: field info (name + value + rule string + numeric flag)
"   EXPORTING: validation result (field name + message + status S/E)
"   EXCEPTIONS: INVALID_PARAMETER (if rule format is incorrect)

  " Add your validation logic here
  " If valid   → set status = 'S', message = 'valid'
  " If invalid → set status = 'E', message = error description

ENDFUNCTION.

No changes are required to ZCL_API_VALIDATOR itself. This is the beauty of the registry pattern: the core class does not need to know anything about the new validator being added.

9. Conclusion

ZCL_API_VALIDATOR brings modern validation patterns into the ABAP ecosystem with several solid design decisions:

  • Registry pattern keeps the class extensible without touching core logic
  • One line per field makes validation code easy to read and review
  • Collect-all-errors gives the API client complete feedback in a single response
  • The numeric flag lets a single validator handle two modes without duplication

This approach will feel familiar to developers who have worked with Golang, Laravel, or other modern frameworks — which is exactly where the inspiration came from. And that is the added value: bringing a proven pattern into the ABAP ecosystem that has long lacked a standard like this.