PLSQL. JSON validator (string validation)

Как известно, JSON формат представляет собой альтернативу XML формату. В настоящее время данный формат получил довольно широкое распространение. Так же и Oracle не отстает от времени и в версии 12.1.0.2 была добавлена нативная поддержка JSON формата. Если Вы работаете на более поздних версиях, то можно добавить собственные реализации для поддержки формата. Есть уже и готовые решения, например можно воспользоваться следующим ресурсом - https://github.com/pljson/pljson.

В моем же случае все немного проще. На стороне сервера по триггеру запускался обработчик, который формировал данные в строковом виде в JSON формате и после отправлял POST запросом на определенный ресурс. Перед отправкой необходимо было убедиться в валидности JSON формата. Так возникла потребность в организации такого рода JSON валидатора. За основу был взят JSON парсер, написанный Дугласом Крокфордом на javascript, с которым можно ознакомиться по ссылке - https://github.com/douglascrockford/JSON-js/blob/master/json_parse.js. Данный парсер предоставляет альтернативу встроенному парсеру JSON.parse в случае отсутствия поддержки оного в Вашем браузере. Ниже представлена реализация тела пакета данного валидатора. В спецификации внешний доступ предоставлен трем методам:

function unsafety_validate(source in varchar2) return boolean;
function safety_validate(source in varchar2) return boolean;
function safety_validate(source in varchar2, errmsg in out varchar2) return boolean;

Префиксы - safety и unsafety - указывают на возможность генерирования возникающих в ходе валидации исключений. С полным описанием пакета можно ознакомиться по ссылке - https://github.com/mozg1984/PL-JSON-VALIDATOR.


create or replace package body json_validator is

  /* @private
   * type for instantiate keys storages
   */
  type array_t is table of varchar2(1000);

  atCh integer; -- the index of the current character
  ch char;      -- the current character
  text varchar2(32767);

  /* @private
   * call error when something is wrong
   */
  procedure error(m in varchar2 default '')
  is
    errmsg varchar2(32767);
  begin
    errmsg := 'name: SyntaxError, ';
    errmsg := errmsg || 'message: ' || m || ', ';
    errmsg := errmsg || 'at: ' || atCh || ', ';
    errmsg := errmsg || 'text: ' || text;
    raise_application_error(-20000, errmsg);
  end;

  /* @private
   * escape sequence
   */
  function escapee(ch in char default '') return varchar2
  is
  begin
    case ch
      when '"' then return '"';
      when '\' then return '\';
      when '/' then return '/';
      when 'b' then return '\b';
      when 'f' then return '\f';
      when 'n' then return '\n';
      when 'r' then return '\r';
      when 't' then return '\t';
      else return null;
    end case;
  end;

  /* @private
   * add key to keys storage
   */
  procedure add_key(p_array in out nocopy array_t, p_key in varchar2)
  is
  begin
    p_array.extend();
    p_array(p_array.count()) := p_key;
    exception
      when collection_is_null then 
        p_array := array_t(p_key);
  end;
  
  /* @private
   * check duplicate keys in keys storage
   */
  function has_key(p_array in out nocopy array_t, p_key in varchar2) return boolean
  is
    is_equal boolean := false;
  begin
    for i in p_array.first..p_array.last loop
      if p_array(i) = p_key then
        is_equal := true;
      end if; 
    end loop;
    
    return is_equal;
    exception
      when collection_is_null then 
        return is_equal;
  end;

  /* @private
   *  get char from string by index position
   */
  function charAt(str in varchar2, pos in integer) return char
  is
  begin
    return substr(str, pos, 1);
  end;

  /* @private
   * check if given number
   */
  function isNumber(num in varchar2) return boolean
  is
  begin
    case regexp_like(num, '^\d*\.?\d+$')
      when true then 
        return true;
      else 
        return false; 
    end case;
  end;
  
  /* @private
   * check if given character
   */
  function exist(ch in varchar2 default '') return boolean
  is
  begin
    return ch is not null; 
  end;

  /* @private
   * convert hex number (character) to decimal number
   */
  function hex2dec(ch in char) return number
  is
  begin
    return to_number(ch, 'x');
    exception
      when value_error then
        return null;
  end;
  
  /* @private
   * convert hex code to unicode character
   */
  function getUnicodeChar(hexcode in varchar2) return varchar2
  is
  begin
    return trim(unistr('\' || hexcode || ' '));
    exception
      when others then
        return ''; 
  end;

  /* @private
   * get the next character
   */
  function nextChar(c in char default '') return char
  is
  begin
    -- if a c parameter is provided, verify that it matches the current character
    if (exist(c) and c != ch) then
      error('Expected ''' || c || ''' instead of ''' || ch || '''');
    end if;

    -- when there are no more characters, return the empty string
    ch := charAt(text, atCh);
    atCh := atCh + 1;
    return ch;
  end;
  
  /* @private
   * move current char to next
   */
  procedure nextChar(c in char default '')
  is
  begin
    -- if a c parameter is provided, verify that it matches the current character
    if (exist(c) and c != ch) then
      error('Expected ''' || c || ''' instead of ''' || ch || '''');
    end if;

    ch := charAt(text, atCh);
    atCh := atCh + 1;
  end;

  /* @private
   * forward declaration of common validate function 
   */
  function validate return boolean;

  /* @private
   * validate a number value
   */
  function validateNumber return boolean
  is
    l_number varchar2(32767) := '';
  begin
    if (ch = '-') then
      l_number := '-';
      nextChar('-');
    end if;

    while (ch >= '0' and ch <= '9') loop
      l_number := l_number || ch;
      nextChar();
    end loop;

    if (ch = '.') then
      l_number := l_number || '.';
      while (exist(nextChar()) and ch >= '0' and ch <= '9') loop
        l_number := l_number || ch;
      end loop;
    end if;

    if (ch = 'e' or ch = 'E') then
      l_number := l_number || ch;
      nextChar();

      if (ch = '-' or ch = '+') then
        l_number := l_number || ch;
        nextChar();
      end if;

      while (ch >= '0' and ch <= '9') loop
        l_number := l_number || ch;
        nextChar();
      end loop;
    end if;

    if (not(isNumber(l_number))) then
      error('Bad number');
    else
      return true;
    end if;
  end;

  /* @private
   * validate a string value
   */
  function validateString return boolean
  is
    decnum number;
  begin
    -- when parsing for string values, we must look for " and \ characters
    if (ch = '"') then
      while (exist(nextChar())) loop
        if (ch = '"') then
          nextChar();
          return true;
        end if;

        if (ch = '\') then
          nextChar();
          if (ch = 'u') then
            for i in 1..4 loop
              decnum := hex2dec(nextChar());              
              if (not isNumber(decnum)) then
                error('Malformed Unicode character escape sequence');
              end if;
            end loop;
          elsif (escapee(ch) is not null) then
            null;
          else
            exit;
          end if;
        end if;
      end loop;
    end if;
    error('Bad string');
  end;
  
  /* @private
   * get string key value (for checking duplicate keys in object value)
   */
  function stringKey return varchar2
  is
    l_string varchar2(32767) := '';
    decnum number;
    uffff varchar2(32767);
  begin
    -- when parsing for string values, we must look for " and \ characters
    if (ch = '"') then
      while (exist(nextChar())) loop
        if (ch = '"') then
          nextChar();
          return l_string;
        end if;

        if (ch = '\') then
          nextChar();
          if (ch = 'u') then
            uffff := '';
            for i in 1..4 loop
              decnum := hex2dec(nextChar());
              if (not isNumber(decnum)) then
                error('Malformed Unicode character escape sequence');
              end if;
              uffff := uffff || ch;
            end loop;
            l_string := l_string || getUnicodeChar(uffff);
          elsif (escapee(ch) is not null) then
            l_string := l_string || escapee(ch);
          else
            exit;
          end if;
        else
          l_string := l_string || ch;
        end if;
      end loop;
    end if;
    error('Bad string');
  end;
  
  /* @private
   * skip whitespace
   */
  procedure white 
  is
  begin
    while (exist(ch) and ch <= ' ') loop
      nextChar();
    end loop;
  end;

  /* @private
   * validate a word (true, false or null)
   */
  function validateWord return boolean
  is
  begin
    case ch
      when 't' then
        nextChar('t');
        nextChar('r');
        nextChar('u');
        nextChar('e');
        return true;
      when 'f' then
        nextChar('f');
        nextChar('a');
        nextChar('l');
        nextChar('s');
        nextChar('e');
        return true;
      when 'n' then
        nextChar('n');
        nextChar('u');
        nextChar('l');
        nextChar('l');
        return true;
      else
        error('Unexpected ''' || ch || '''');
    end case; 
  end;
  
  /* @private
   * validate an array value
   */
  function validateArray return boolean
  is
    l_result boolean := true;
  begin
    if (ch = '[') then
      nextChar('[');
      white();
      if (ch = ']') then
        nextChar(']');
        return l_result; -- empty array
      end if;
      
      while (exist(ch)) loop
        l_result := validate();
        white();
        if (ch = ']') then
          nextChar(']');
          return l_result;
        end if;
        nextChar(',');
        white();
      end loop;
    end if;
    error('Bad array');
  end;

  /* @private
   * validate an object value
   */
  function validateObject return boolean
  is
    l_result boolean := true;
    l_key varchar2(1000);
    l_keys array_t;
  begin
    if (ch = '{') then
      nextChar('{');
      white();
      if (ch = '}') then
        nextChar('}');
        return l_result; -- empty object
      end if;
      
      while (exist(ch)) loop
        l_key := stringKey();
        white();
        nextChar(':');
        
        if (has_key(l_keys, l_key)) then
          error('Duplicate key "' || l_key || '"');
        end if;
        
        add_key(l_keys, l_key);
        l_result := validate();
        white();
        
        if (ch = '}') then
          nextChar('}');
          return l_result;
        end if;
        
        nextChar(',');
        white();
      end loop;
    end if;
    error('Bad object');
  end;

  /* @public
   * validate a JSON value. 
   * It could be an object, an array, a string, a number, or a word.
   */
  function validate return boolean
  is
  begin
    white();
    case ch
      when '{' then
        return validateObject();
      when '[' then
        return validateArray();
      when '"' then
        return validateString();
      when '-' then
        return validateNumber();
      else
        return case
          when (ch >= '0' and ch <= '9') then validateNumber() else validateWord() end; 
    end case;
  end;
  
  /* @public
   * unsafety validate JSON string (throw exception ora-20000)
   */
  function unsafety_validate(source in varchar2) return boolean
  is
    l_result boolean;
  begin
    text := source;
    atCh := 1;
    ch := ' ';
    
    l_result := validate();
    white();
    
    if (exist(ch)) then
      error('Syntax error');
    end if;
    
    return l_result; 
  end;
  
  /* @public
   * safety validate JSON string (catch all exceptions)
   */
  function safety_validate(source in varchar2) return boolean
  is
    l_result boolean;
  begin
    text := source;
    atCh := 1;
    ch := ' ';
    
    l_result := validate();
    white();
    
    if (exist(ch)) then
      error('Syntax error');
    end if;
    
    return l_result;
    
    exception
      when others then
        return false;
  end;
  
  /* @public
   * safety validate JSON string (catch all exceptions with error message) 
   */
  function safety_validate(source in varchar2,
                           errmsg in out varchar2) return boolean
  is
    l_result boolean;
  begin
    text := source;
    atCh := 1;
    ch := ' ';
    
    l_result := validate();
    white();
    
    if (exist(ch)) then
      error('Syntax error');
    end if;
    
    return l_result;
    
    exception
      when others then
        errmsg := sqlerrm;
        return false;
  end;

begin
  -- init
  null;
end json_validator;


Пример использования данного пакета


declare
  json_string varchar2(1000);
  errmsg varchar2(32767);
begin
  
  json_string := '{"number": 123456, "text": "active", "array": [], "object": {}}';
  
  if json_validator.safety_validate(json_string, errmsg) then
    dbms_output.put_line('JSON is valid');
  else
    dbms_output.put_line('JSON is not valid');
    dbms_output.put_line(errmsg);
  end if;
  
end;