Конечно, да, скажете вы. Но не было бы этой статьи, если бы не было вопроса.

Так же эта статья будет вам полезна, если вы используете эквайринг от Тинькофф.

Немного предыстории. Какое-то время назад на одном из своих проектов я поменял онлайн-эквайринг на Тинькофф и уже после отладки начали всплывать странные ошибки с оплатой: хэш-подпись запроса считалась неверно. Причем, только на реальных платежах, что еще больше омрачало ситуацию.

Проблема оказалась в поле Data json-объекта, который приходит от банка в нотификации платежа. В одних случаях оно приходит как Data, а в других DATA (в верхнем регистре).

О проблеме я сообщил в поддержку Тинькофф и тут началось интересное. Поддержка утверждает, что регистр ключей в JSON не играет никакой роли:

Валерий, здравствуйте, JSON не является регистрозависимым. С его точки зрения DATA и Data это одно и тоже. Корень проблемы тут в вашем программном обеспечении, которое как раз регистрозависимое.

К сожалению, доказать обратное техническим специалистам Тинькофф не удалось. Найти упоминание регистрозависимости JSON в спецификации - еще более сложная задача (нашли), поэтому я поступил так, как делаю всегда: пишу код и проверяю как он работает.

Поскольку JSON сам по себе ничего не значит, а всегда обрабатывается каким-либо языком программирования, можно легко узнать, как те или иные языки относятся к ключам в разном регистре в JSON.

JavaScript - как самый популярный

Когда говорят про JSON, самым популярным языком, его использующим, является JS.

const json = JSON.parse('{"Data":1,"DATA":2}');
console.log(json, json.Data); // {Data: 1, DATA: 2} 1

Как видим, регистр ключей играет роль. Два разных ключа с разными значениями. И если в документации указан ключ Data, то DATA не подойдет, увы.

PHP - когда нет JS

Возможно будет разница между массивом и объектом?

<?php
$json = json_decode('{"Data":1,"DATA":2}', true);
var_dump($json);
// array (size=2)
//  'Data' => int 1
//  'DATA' => int 2

$json = json_decode('{"Data":1,"DATA":2}', false);
var_dump($json);
// object(stdClass)[253]
//  public 'Data' => int 1
//  public 'DATA' => int 2

Ситуация аналогичная. Ключи в разном регистре - это разные ключи. Преобразование в массив или в объект роли не играет.

Go - язык со строгой типизацией

Все же JS и PHP позволяют много вольностей. Возможно компилируемый язык со строгой типизацией будет вести себя иначе?

var data map[string]interface{}
json.Unmarshal([]byte("{\"Data\":1,\"DATA\":2}"), &data)
fmt.Println(data) // map[DATA:2 Data:1]

И снова ключи в разном регистре - это разные ключи. Мы получили map с двумя разными ключами и значениями.

C# - включаемая регистронезависимость

В комментариях выложили пример кода для C#

Поведение либо как у других языков, либо можно включить регистронезависимость и ловить ошибки при наложении свойств (хотя в случае одного свойства и правда не будет разницы).

Заключение

Если вы все еще сомневаетесь, является ли JSON регистрозависимым, просто попробуйте аналогичный код в своем языке программирования.

Я не встречал языка, в котором JSON был бы регистронезависимым. Если вы такой знаете - сообщите, будет интересно.

P.S. Я уверен, что разработчики Тинькофф тоже читают хабр. Обратите пожалуйста внимание на проблему. У вас на проде случайным образом меняется регистр ключа Data в нотификации платежа.

UPD 19.04.22 14:50

Спасибо @Busla за найденное упоминание регистрозависимости в спецификации.

Комментарии (23)


  1. leovi
    19.04.2022 14:35
    +5

    JSON это javascript object notation. Javascript регистрозависимый.Следовательно и ключи JSON объекта регистрозависимые. То что они представлены в виде строк, я думаю, роли не играет.


    1. Akuma Автор
      19.04.2022 14:37
      +2

      Это доказательство ради доказательства, скорее.

      Так-то все очевидно, тем кто хоть раз работал с JSON (а это почти все программисты в вебе)


      1. SadOcean
        19.04.2022 17:37
        +2

        Нет, почему же.

        Язык js - это стандарт

        Образованный от него json - тоже подчиняется правилам стандарта.

        То, что некоторые реализации игнорируют или позволяют обходить стандарт - это их прихоть, не имеющая отношения к json

        Пусть и удобная.

        Так что можно с уверенностью сказать - ключи в json регистрозависимые


  1. InChaos
    19.04.2022 14:36

    Ваш пример не корректен. То что строки во всех ЯП всегда сохраняют регистр, это не значит что метки в JSON должны быть регистрозависимыми, одно из другого никак не вытекает.

    В части XML например, в спецификации четко указано, что теги регистрозависимы, по поводу JSON такого найти не смог. Зато нашел интересную информацию: C# – Case sensitivity in JSON deserialization - "By default Newtonsoft does case insensitive JSON deserialization and System.Text.Json does case sensitive JSON deserialization". Получается кто как хочет так и делает.

    https://makolyte.com/csharp-case-sensitivity-in-json-deserialization/

    И кстати тут ответ на Ваш вопрос "Я не встречал языка, в котором JSON был бы регистронезависимым. Если вы такой знаете - сообщите, будет интересно."


    1. Akuma Автор
      19.04.2022 14:39

      Не имею возможности на C# проверить, к сожалению. Можете такой же пример из статьи воспроизвести?

      Теория тут не особо уместна, как мне кажется. Делают как хотят, но работают с этим сторонние люди и такие детские проблемы - для банка не серьезно.


    1. SergeiMinaev
      19.04.2022 14:42
      -2

      Не удивительно, что такое поведение именно в C#. Отголосок Windows, в которой имена файлов тоже регистронезависимые.


      1. InChaos
        19.04.2022 14:59
        +1

        Соглашусь, но не полностью. В том же XML четко описано поведение - регистрозависимо и точка будь то Windows или Linux, а в JSON-е отсутствует требование, которое каждый трактует индивидуально. В компоненте Newtonsoft например настраиваемо, но по дефолту регистронезависимо, никто не мешает изменить.
        options.PropertyNameCaseInsensitive = true;



      1. dmitryvolochaev
        19.04.2022 16:01

        Проверил C#. Вот так получается регистрозависимо:

        using System;
        using System.Text.Json;
        
        namespace Test1
        {
            class DataContainer
            {
                public int data { get; set; }
            }
        
            class Program
            {
                static void Main(string[] args)
                {
                    var c0 = JsonSerializer.Deserialize<DataContainer>("{ \"data\": 2 }");
                    var c1 = JsonSerializer.Deserialize<DataContainer>("{ \"Data\": 2 }");
                    var c2 = JsonSerializer.Deserialize<DataContainer>("{ \"DATA\": 2 }");
                    var c3 = JsonSerializer.Deserialize<DataContainer>("{ \"daTA\": 2 }");
                    Console.WriteLine("data: {0}, Data: {1}, DATA: {2}, daTA {3}", c0.data, c1.data, c2.data, c3.data);
                }
            }
        }

        Выводит:

        data: 2, Data: 0, DATA: 0, daTA 0

        Теперь включаем регистронезависимость:

        using System;
        using System.Text.Json;
        
        namespace Test1
        {
            class DataContainer
            {
                public int data { get; set; }
            }
        
            class Program
            {
                static void Main(string[] args)
                {
                    JsonSerializerOptions opt = new JsonSerializerOptions()
                    {
                        PropertyNameCaseInsensitive = true
                    };
        
                    var c0 = JsonSerializer.Deserialize<DataContainer>("{ \"data\": 2 }", opt);
                    var c1 = JsonSerializer.Deserialize<DataContainer>("{ \"Data\": 2 }", opt);
                    var c2 = JsonSerializer.Deserialize<DataContainer>("{ \"DATA\": 2 }", opt);
                    var c3 = JsonSerializer.Deserialize<DataContainer>("{ \"daTA\": 2 }", opt);
                    Console.WriteLine("data: {0}, Data: {1}, DATA: {2}, daTA {3}", c0.data, c1.data, c2.data, c3.data);
                }
            }
        }

        Выводит:

        data: 2, Data: 2, DATA: 2, daTA 2


        1. dmitryvolochaev
          19.04.2022 16:11

          А если есть свойства, отличающиеся только регистром, а мы включаем регистронезависимость, то получим ошибку

          using System;
          using System.Text.Json;
          
          namespace Test1
          {
              class DataContainer
              {
                  public int data { get; set; }
                  public int DATA { get; set; }
              }
          
              class Program
              {
                  static void Main(string[] args)
                  {
                      JsonSerializerOptions opt = new JsonSerializerOptions()
                      {
                          PropertyNameCaseInsensitive = true
                      };
          
                      var c0 = JsonSerializer.Deserialize<DataContainer>("{ \"data\": 2 }", opt);
                      var c1 = JsonSerializer.Deserialize<DataContainer>("{ \"Data\": 2 }", opt);
                      var c2 = JsonSerializer.Deserialize<DataContainer>("{ \"DATA\": 2 }", opt);
                      var c3 = JsonSerializer.Deserialize<DataContainer>("{ \"daTA\": 2 }", opt);
                      Console.WriteLine("data: {0}, {1}, Data: {2}, {3}, DATA: {4}, {5}, daTA {6}, {7}", c0.data, c0.DATA, c1.data, c1.DATA, c2.data, c2.DATA, c3.data, c3.DATA);
                  }
              }
          }

          Кидает InvalidOperationException с текстом "The JSON property name for 'Test1.DataContainer.DATA' collides with another property."


          1. Akuma Автор
            19.04.2022 16:23

            Спасибо, добавлю в статью.


      1. radtie
        19.04.2022 16:28
        +1

        Никакой связи. Я думаю, основная причина в различной нотации принятой в Javascript/JSON (camelCase) и C# (PascalCase). Чтобы можно было автоматически сериализировать шарповые DTO`шки


        1. dmitryvolochaev
          19.04.2022 16:34

          Чтобы делать разную нотацию, используется атрибут [JsonPropertyName("data")]


          1. radtie
            19.04.2022 16:49

            Newtonsoft каких только средств конфигурирования не имеет, но это не отменяет тот факт, что кому то проще оставить так, чем маркировать все поля в сотнях классов и следить за их консистентностью.


  1. Busla
    19.04.2022 14:47
    +6

    Найти упоминание регистрозависимости JSON в спецификации - еще более сложная задача

    да ладно, это в rfc8259 описано отдельным параграфом:

    8.3. String Comparison

    Software implementations are typically required to test names of object members for equality. Implementations that transform the textual representation into sequences of Unicode code units and then perform the comparison numerically, code unit by code unit, are interoperable in the sense that implementations will agree in all cases on equality or inequality of two strings.

    Escape-последовательности раскрываются в Unicode, а затем такие подготовленные строки бинарно сравниваются. Т.е. имена членов объектов регистрозависимы.


    1. Akuma Автор
      19.04.2022 14:48

      Отлично. А я не нашел. Спасибо :)


      1. InChaos
        19.04.2022 15:08

        Справедливости ради это не относится к требованию к тегам, а лишь к сравнению строк!
        Почитайте например п 8.2.
        " Однако ABNF в этой спецификации разрешает имена элементов и значения строк с битовыми последовательностями, которые не могут быть представлены символами Unicode, например, "\uDEAD" (один непарный суррогат UTF-16). Такие экземпляры могут встречаться, например, при отсечке библиотекой строк UTF-16 без проверки разрыва суррогатных пар. Поведение программы, получившей текст JSON с такими значениями, не предсказуемо. "
        Перевод RFC - https://www.protokols.ru/WP/wp-content/uploads/2017/12/rfc8259.pdf


        1. Akuma Автор
          19.04.2022 15:12
          +1

          Я так же прекрасно понимаю, что тут (как и всегда) действует принцип "что хочу, то и ворочу". Каждый ЯП может по своему реализовать JSON. Более того, даже разные библиотеки могут по разному парсить JSON. Но все же я не встречал регистронезависимых ключей.

          Как у вас и указано "Поведение программы, получившей текст JSON с такими значениями, не предсказуемо" - как раз поэтому, мне кажется, строковые ключи - это обычные строки, а значит регистрозависимые.


          1. Serge3leo
            19.04.2022 18:16
            +1

            RFC 8259 нормативно ссылается на ECMA-404, в котором явно определяется что:

            «...The JSON syntax does not impose any restrictions on the strings used as names, does not require that name strings be unique, and does not assign any significance to the ordering of name/value pairs. These are all semantic considerations that may be defined by JSON processors or in specifications defining specific uses of JSON for data interchange....»

            Т.е. могут встретиться системы обмена информацией в формате JSON, полностью соответствующие стандартам ECMA-404 и RFC 8259, но у которых имена полей объекта не зависят от регистра или вообще допускают перевод на национальные языки.


  1. Arty_Fact
    19.04.2022 14:58
    +5

    Непонятно, с чего бы он должен быть регистронезависимым. На json.org говорится:

    A string is a sequence of zero or more Unicode characters, wrapped in double quotes, using backslash escapes. A character is represented as a single character string. A string is very much like a C or Java string.
    Символы d и D — это разные символы юникода.
    Смотрим RFC8259:
    Implementations that transform the textual representation into sequences of Unicode code units and then perform the comparison numerically, code unit by code unit, are interoperable in the sense that implementations will agree in all cases on equality or inequality of two strings.
    То есть напрямую рекомендуют сравнивать коды символов, которые у заглавной и строчной — разные.


  1. ky0
    19.04.2022 15:00
    -3

    Поддержка утверждает, что регистр ключей в JSON не играет никакой роли

    Это не та же самая поддержка, которая недавно утверждала, что операция «прокручивания» валют, оставляющая клиентов в существенном плюсе — норм?


  1. zartdinov
    19.04.2022 15:01

    Ну если так считают, то могли бы и поправить. Все равно ничего не сломается, по их мнению.


  1. shalamberidze
    20.04.2022 19:56

    :) Можно так :)

    <sarcasm>

    jsonString = jsonString.toUpperCase()

    Object object = deserialize

    </sarcasm>