Ошибка десериализации json

Время на прочтение
9 мин

Количество просмотров 42K

image

Недавно мне довелось разрабатывать на Go http-клиент для сервиса, предоставляющего REST API с json-ом в роли формата кодирования. Стандартная задача, но в ходе работы мне пришлось столкнуться с нестандартной проблемой. Рассказываю в чем суть.

Как известно, формат json имеет типы данных. Четыре примитивных: строка, число, логический, null; и два структурных типа: объект и массив. В данном случае нас интересуют примитивные типы. Вот пример json кода с четырьмя полями разных типов:

{
	"name":"qwerty",
	"price":258.25,
	"active":true,
	"description":null,
}

Как видно в примере, строковое значение заключается в кавычки. Числовое — не имеет кавычек. Логический тип может иметь только одно из двух значений: true или false (без кавычек). И тип null соответственно имеет значение null (также без кавычек).

А теперь собственно сама проблема. В какой-то момент, при детальном рассмотрении получаемого от стороннего сервиса json-кода, я обнаружил, что одно из полей (назовем его price) помимо числового значения периодически имеет строковое значение (число в кавычках). Т. е. один и тот же запрос с разными параметрами может вернуть число в виде числа, а может вернуть это же число в виде строки. Ума не приложу, как на том конце организован код, возвращающий такие результаты, но видимо, это связано с тем, что сервис сам является агрегатором и тянет данные из разных источников, а разработчики не привели json ответа сервера к единому формату. Тем не менее, надо работать с тем что есть.

Но далее меня ждало еще большее удивление. Логическое поле (назовем его active), помимо значений true и false, возвращало строковые значения «true», «false», и даже числовые 1 и 0 (истина и ложь соответственно).

Вся эта путаница с типами данных не была бы критичной, если бы я обрабатывал json скажем на слаботипизированном PHP, но Go имеет сильную типизацию, и требует четкого указания типа десериализуемого поля. В итоге возникла необходимость реализовать механизм, позволяющий в процессе десериализации преобразовывать все значения поля active в логический тип, и любые значения поля price — в числовой.

Начнем с числового поля price.

Предположим что у нас есть json-код следующего вида:

[
	{"id":1,"price":2.58},
	{"id":2,"price":7.15}
]

Т. е. json содержит массив объектов с двумя полями числового типа. Стандартный код десериализации данного json-а на Go выглядит так:

type Target struct {
	Id    int     `json:"id"`
	Price float64 `json:"price"`
}

func main() {
	jsonString := `[{"id":1,"price":2.58},
					{"id":4,"price":7.15}]`

	targets := []Target{}

	err := json.Unmarshal([]byte(jsonString), &targets)
	if err != nil {
		fmt.Println(err)
		return
	}

	for _, t := range targets {
		fmt.Println(t.Id, "-", t.Price)
	}
}

В данном коде мы десериализуем поле id в тип int, а поле price в тип float64. Теперь предположим, что наш json-код выглядит так:

[
	{"id":1,"price":2.58},
	{"id":2,"price":"2.58"},
	{"id":3,"price":7.15},
	{"id":4,"price":"7.15"}
]

Т. е. поле price содержит значения как числового типа, так и строкового. В данном случае только числовые значения поля price могут быть декодированы в тип float64, строковые же значения вызовут ошибку о несовместимости типов. Это значит, что ни float64, ни любой другой примитивный тип не подходят для десериализации данного поля, и нам необходим свой пользовательский тип со своей логикой десериализации.

В качестве такого типа объявим структуру CustomFloat64 с единственным полем Float64 типа float64.

type CustomFloat64 struct{
	Float64 float64
}

И сразу укажем данный тип для поля Price в структуре Target:

type Target struct {
	Id    int           `json:"id"`
	Price CustomFloat64 `json:"price"`
}

Теперь необходимо описать собственную логику декодирования поля c типом CustomFloat64.

В пакете «encoding/json» предусмотрены два специальных метода: MarshalJSON и UnmarshalJSON, которые и предназначены для кастомизации логики кодирования и декодирования конкретного пользовательского типа данных. Достаточно переопределить эти методы и описать собственную реализацию.

Переопределим метод UnmarshalJSON для произвольного типа CustomFloat64. При этом необходимо строго следовать сигнатуре метода, иначе он просто не сработает, а главное не выдаст при этом ошибку.

func (cf *CustomFloat64) UnmarshalJSON(data []byte) error {

На входе данный метод принимает слайс байт (data), в котором содержится значение конкретного поля декодируемого json. Если преобразовать данную последовательность байт в строку, то мы увидим значение поля именно в том виде, в каком оно записано в json. Т. е. если это строковый тип, то мы увидим именно строку с двойными кавычками («258»), если числовой тип, то увидим строку без кавычек (258).

Чтобы отличить числовое значение от строкового, необходимо проверить, является ли первый символ кавычкой. Так как символ двойной кавычки в таблице UNICODE занимает один байт, нам достаточно проверить первый байт слайса data, сравнив его с номером символа в UNICODE. Это номер 34. Обратите внимание, что в общем случае, символ не равнозначен байту, так как может занимать больше одного байта. Символу в Go равнозначен тип rune (руна). В нашем же случае достаточно данного условия:

if data[0] == 34 {

Если условие выполняется, то значение имеет строковый тип, и нам необходимо получить строку между кавычками, т. е. слайс байт между первым и последним байтом. Именно в этом слайсе содержится числовое значение, которое может быть декодировано в примитивный тип float64. Это значит, что мы можем применить к нему метод json.Unmarshal, при этом результат сохраняя в поле Float64 структуры CustomFloat64.

err := json.Unmarshal(data[1:len(data)-1], &cf.Float64)

Если же слайс data начинается не с кавычки, то значит в нем уже содержится числовой тип данных, и мы можем применить метод json.Unmarshal непосредственно ко всему слайсу data.

err := json.Unmarshal(data, &cf.Float64)

Вот полный код метода UnmarshalJSON:

func (cf *CustomFloat64) UnmarshalJSON(data []byte) error {
	if data[0] == 34 {
		err := json.Unmarshal(data[1:len(data)-1], &cf.Float64)
		if err != nil {
			return errors.New("CustomFloat64: UnmarshalJSON: " + err.Error())
		}
	} else {
		err := json.Unmarshal(data, &cf.Float64)
		if err != nil {
			return errors.New("CustomFloat64: UnmarshalJSON: " + err.Error())
		}
	}
	return nil
}

В итоге, с применением метода json.Unmarshal к нашему json-коду, все значения поля price будут прозрачно для нас преобразованы в примитивный тип float64, и результат запишется в поле Float64 структуры CustomFloat64.

Теперь нам может понадобиться преобразовать структуру Target обратно в json. Но, если мы применяем метод json.Marshal непосредственно к типу CustomFloat64, то сериализуем данную структуру в виде объекта. Нам же необходимо кодировать поле price в числовое значение. Чтобы кастомизировать логику кодирования пользовательского типа CustomFloat64, реализуем для него метод MarshalJSON, при этом строго соблюдая сигнатуру метода:

func (cf CustomFloat64) MarshalJSON() ([]byte, error) {
	json, err := json.Marshal(cf.Float64)
	return json, err
}

Все, что нужно сделать в этом методе, это опять же использовать метод json.Marshal, но уже применять его не к структуре CustomFloat64, а к ее полю Float64. Из метода возвращаем полученный слайс байт и ошибку.

Вот полный код с выводом результатов сериализации и десериализации (проверка ошибок опущена для краткости, номер байта с символом двойных кавычек вынесен в константу):

package main

import (
	"encoding/json"
	"errors"
	"fmt"
)

type CustomFloat64 struct {
	Float64 float64
}

const QUOTES_BYTE = 34

func (cf *CustomFloat64) UnmarshalJSON(data []byte) error {
	if data[0] == QUOTES_BYTE {
		err := json.Unmarshal(data[1:len(data)-1], &cf.Float64)
		if err != nil {
			return errors.New("CustomFloat64: UnmarshalJSON: " + err.Error())
		}
	} else {
		err := json.Unmarshal(data, &cf.Float64)
		if err != nil {
			return errors.New("CustomFloat64: UnmarshalJSON: " + err.Error())
		}
	}
	return nil
}

func (cf CustomFloat64) MarshalJSON() ([]byte, error) {
	json, err := json.Marshal(cf.Float64)
	return json, err
}

type Target struct {
	Id    int           `json:"id"`
	Price CustomFloat64 `json:"price"`
}

func main() {
	jsonString := `[{"id":1,"price":2.58},
					{"id":2,"price":"2.58"},
					{"id":3,"price":7.15},
					{"id":4,"price":"7.15"}]`

	targets := []Target{}

	_ := json.Unmarshal([]byte(jsonString), &targets)

	for _, t := range targets {
		fmt.Println(t.Id, "-", t.Price.Float64)
	}

	jsonStringNew, _ := json.Marshal(targets)
	fmt.Println(string(jsonStringNew))
}

Результат выполнения кода:

1 - 2.58
2 - 2.58
3 - 7.15
4 - 7.15
[{"id":1,"price":2.58},{"id":2,"price":2.58},{"id":3,"price":7.15},{"id":4,"price":7.15}]

Перейдем ко второй части и реализуем аналогичный код для десериализации json-а с несогласованными значениями логического поля.

Предположим что у нас есть json-код следующего вида:

[
	{"id":1,"active":true},
	{"id":2,"active":"true"},
	{"id":3,"active":"1"},
	{"id":4,"active":1},
	{"id":5,"active":false},
	{"id":6,"active":"false"},
	{"id":7,"active":"0"},
	{"id":8,"active":0},
	{"id":9,"active":""}
]

В данном случае поле active подразумевает логический тип и наличие только одного из двух значений: true и false. Значения не логического типа необходимо будет преобразовать в логический в ходе десериализации.

В текущем примере мы допускаем следующие соответствия. Значению true соответствуют: true (логическое), «true» (строковое), «1» (строковое), 1 (числовое). Значению false соответствуют: false (логическое), «false» (строковое), «0» (строковое), 0 (числовое), «» (пустая строка).

Для начала объявим целевую структуру десериализации. В качестве типа поля Active сразу указываем пользовательский тип CustomBool:

type Target struct {
	Id     int        `json:"id"`
	Active CustomBool `json:"active"`
}

CustomBool является структурой с одним единственным полем Bool типа bool:

type CustomBool struct {
	Bool bool
}

Реализуем для данной структуры метод UnmarshalJSON. Сразу приведу код:

func (cb *CustomBool) UnmarshalJSON(data []byte) error {
	switch string(data) {
	case `"true"`, `true`, `"1"`, `1`:
		cb.Bool = true
		return nil
	case `"false"`, `false`, `"0"`, `0`, `""`:
		cb.Bool = false
		return nil
	default:
		return errors.New("CustomBool: parsing "" + string(data) + "": unknown value")
	}
}

Так как поле active в нашем случае имеет ограниченное количество значений, мы можем с помощью конструкции switch-case принять решение, о том, чему должно быть равно значение поля Bool структуры CustomBool. Для проверки понадобится всего два блока case. В первом блоке мы проверяем значение на соответствие true, во втором — false.

При записи возможных значений, следует обратить внимание на роль грависа (это такая кавычка на клавише с буквой Ё в английской раскладке). Данный символ позволяет экранировать двойные кавычки в строке. Для наглядности данным символом я обрамил и значения с кавычками и без кавычек. Таким образом, `false` соответствует строке false (без кавычек, тип bool в json), а `«false»` соответствует строке «false» (с кавычками, тип string в json). Тоже самое и со значениями `1` и `«1»` Первое — это число 1 (в json записано без кавычек), второе — строка «1» (в json записано с кавычками). Вот эта запись `»»` — это пустая строка, Т. е. в формате json она выглядит так: «».

Соответствующее значение (true или false) мы записываем непосредственно в поле Bool структуры CustomBool:

cb.Bool = true

В блоке defaul возвращаем ошибку о том, что поле имеет неизвестное значение:

return errors.New("CustomBool: parsing "" + string(data) + "": unknown value")

Теперь мы можем применять метод json.Unmarshal к нашему json-коду, и значения поля active будут преобразовываться в примитивный тип bool.

Реализуем метод MarshalJSON для структуры CustomBool:

func (cb CustomBool) MarshalJSON() ([]byte, error) {
	json, err := json.Marshal(cb.Bool)
	return json, err
}

Здесь ничего нового. Метод выполняет сериализацию поля Bool структуры CustomBool.

Вот полный код с выводом результатов сериализации и десериализации (проверка ошибок опущена для краткости):

package main

import (
	"encoding/json"
	"errors"
	"fmt"
)

type CustomBool struct {
	Bool bool
}

func (cb *CustomBool) UnmarshalJSON(data []byte) error {
	switch string(data) {
	case `"true"`, `true`, `"1"`, `1`:
		cb.Bool = true
		return nil
	case `"false"`, `false`, `"0"`, `0`, `""`:
		cb.Bool = false
		return nil
	default:
		return errors.New("CustomBool: parsing "" + string(data) + "": unknown value")
	}
}

func (cb CustomBool) MarshalJSON() ([]byte, error) {
	json, err := json.Marshal(cb.Bool)
	return json, err
}

type Target struct {
	Id     int        `json:"id"`
	Active CustomBool `json:"active"`
}

func main() {
	jsonString := `[{"id":1,"active":true},
					{"id":2,"active":"true"},
					{"id":3,"active":"1"},
					{"id":4,"active":1},
					{"id":5,"active":false},
					{"id":6,"active":"false"},
					{"id":7,"active":"0"},
					{"id":8,"active":0},
					{"id":9,"active":""}]`

	targets := []Target{}

	_ = json.Unmarshal([]byte(jsonString), &targets)

	for _, t := range targets {
		fmt.Println(t.Id, "-", t.Active.Bool)
	}

	jsonStringNew, _ := json.Marshal(targets)
	fmt.Println(string(jsonStringNew))
}

Результат выполнения кода:

1 - true
2 - true
3 - true
4 - true
5 - false
6 - false
7 - false
8 - false
9 - false
[{"id":1,"active":true},{"id":2,"active":true},{"id":3,"active":true},{"id":4,"active":true},{"id":5,"active":false},{"id":6,"active":false},{"id":7,"active":false},{"id":8,"active":false},{"id":9,"active":false}]

Выводы

Во-первых. Переопределение методов MarshalJSON и UnmarshalJSON для произвольных типов данных позволяет кастомизировать сериализацию и десериализацию конкретного поля json-кода. Помимо указанных вариантов использования, данные функции применяются для работы с полями, допускающими значение null.

Во-вторых. Формат текстового кодирования json — это широко используемый инструмент для обмена информацией, и одним из его преимуществ перед другими форматами является наличие типов данных. За соблюдением этих типов надо строго следить.

Способ первый самый простой

Когда нужны все данные из json.

public class Data
{
    public string m { get; set; }
    /* куча проперти */
}

var  data = JsonConvert.DeserializeObject<Dictionary<string, Data>>(json)

Почему тип Dictionary<string, Data>, т.к. у вас именно JObject идет первым (как я это определил ниже), то все элементы на его верхнем уровне считаются JProperty. У каждого есть имя и значение, где в качестве значения может быть друга вложенная структура данных.
В нашем случае словарь по сути это массив этих самых JProperty, где key=имя проперти и value=значение.

Как я опередил что это JObject. Первый уровень не имеет ни одного JProperty и начинается с фигурных скобок. Пример ниже будет уже считаться JArray. Такой формат библиотека newtonsoft считает массивом.

{
    [{
        "m": "StatTrak™ AK-47 | Blue Laminate (Field-Tested)",
        "c": 1
    },
    {
        "m": "StatTrak™ P250 | Valence (Field-Tested)",
        "c": 1
    }]
}

Разбор в словарь также подходит для случаев, когда ваш json имеет похожую структуру, но поля разные. Например как здесь.

[
    {
        "filterType": "PRICE_FILTER",
        "minPrice":"0.01000000",
        "maxPrice":"10000000.00000000",
        "tickSize":"0.01000000"
    },
    {
        "filterType":"LOT_SIZE",
        "minQty":"0.00001000",
        "maxQty":"10000000.00000000",
        "stepSize":"0.00001000"
    },
    {
        "filterType":"MIN_NOTIONAL",
        "minNotional":"10.00000000"
    }
]

В таком случае вам не нужно будет создавать один класс со всеми этими полями.

var  data = JsonConvert.DeserializeObject<List<Dictionary<string, string>>>(json)

В случае если все таки решитесь на отдельный класс, то все проперти, которые не будут найдены в json останутся инициализированы по дефолту.

Второй вариант, но мудренее

var data = JObject.Parse(a)
    .SelectTokens("$.*")
    .Select(o => o.ToObject<Data>())
    .ToArray();

Переменная data будет типом Data[], но значения 1,2 будут потеряны.
Этот вариант применим, когда исходный json очень большой и имеет сложную структуру, вам не нужны все данные из него, то делать структуру 1 в 1 неразумно. В этом случае можно вытащить с помощью xpath нужные элементы. Xpath это путь до элемета/ов. Работает по такому же принципу, как select запрос в бд. С его помощью можно указать путь и условия по которым вы хотите собрать данные.


  • С помощью этого сайта можно практиковать xpath запросы для json структуру
  • А с помощью этого можно получать структуры, которые будет соответствовать вашему json.
  • Здесь можно почитать о всех фишках библиотеки Newtonsoft

I need to convert JSON data that I get from a REST API and convert them to CSV for some analytic. The problem is that the JSON data do not necessarily follow the same content, so I can’t define a type for mapping. This has become a challenge that is taking too much of my time. I have already created some code, but of course it is not working as it throws exception on this line

var data = JsonConvert.DeserializeObject<List<object>>(jsonData);

The error is:

Additional information: Cannot deserialize the current JSON object
(e.g. {«name»:»value»}) into type
‘System.Collections.Generic.List`1[System.Object]’ because the type
requires a JSON array (e.g. [1,2,3]) to deserialize correctly.

To fix this error either change the JSON to a JSON array (e.g. [1,2,3]) or change the deserialized type so that it is a normal .NET type (e.g. not a primitive type like integer, not a collection type like an array or List) that can be deserialized from a JSON object. JsonObjectAttribute can also be added to the type to force it to deserialize from a JSON object.

Path ‘data’, line 2, position 10.

please let me know what I can do to get this going.

A sample of data would be like this, the fields of data can change very often, for example a new field can be added the next day, so I don’t have the liberty to create a .Net class to map the data.

{
  "data": [
    {
      "ID": "5367ab140026875f70677ab277501bfa",
      "name": "Happiness Initiatives - Flow of Communication/Process & Efficiency",
      "objCode": "PROJ",
      "percentComplete": 100.0,
      "plannedCompletionDate": "2014-08-22T17:00:00:000-0400",
      "plannedStartDate": "2014-05-05T09:00:00:000-0400",
      "priority": 1,
      "projectedCompletionDate": "2014-12-05T08:10:21:555-0500",
      "status": "CPL"
    },
    {
      "ID": "555f452900c8b845238716dd033cf71b",
      "name": "UX Personalization Think Tank and Product Strategy",
      "objCode": "PROJ",
      "percentComplete": 0.0,
      "plannedCompletionDate": "2015-12-01T09:00:00:000-0500",
      "plannedStartDate": "2015-05-22T09:00:00:000-0400",
      "priority": 1,
      "projectedCompletionDate": "2016-01-04T09:00:00:000-0500",
      "status": "APR"
    },
    {
      "ID": "528b92020051ab208aef09a4740b1fe9",
      "name": "SCL Health System - full Sitecore implementation (Task groups with SOW totals in Planned hours - do not bill time here)",
      "objCode": "PROJ",
      "percentComplete": 100.0,
      "plannedCompletionDate": "2016-04-08T17:00:00:000-0400",
      "plannedStartDate": "2013-11-04T09:00:00:000-0500",
      "priority": 1,
      "projectedCompletionDate": "2013-12-12T22:30:00:000-0500",
      "status": "CPL"
    }
 ]
}



namespace BusinessLogic
{
    public class JsonToCsv
    {

       public string ToCsv(string jsonData, string datasetName)
       {
          var data = JsonConvert.DeserializeObject<List<object>>(jsonData);
          DataTable table = ToDataTable(data); 
          StringBuilder result = new StringBuilder();

            for (int i = 0; i < table.Columns.Count; i++)
            {
                result.Append(table.Columns[i].ColumnName);
                result.Append(i == table.Columns.Count - 1 ? "n" : ",");
            }

            foreach (DataRow row in table.Rows)
            {
                for (int i = 0; i < table.Columns.Count; i++)
                {
                    result.Append(row[i].ToString());
                    result.Append(i == table.Columns.Count - 1 ? "n" : ",");
                }
            }

            return result.ToString().TrimEnd(new char[] {'r', 'n'});

        }

        private DataTable ToDataTable<T>( IList<T> data )
            {
            PropertyDescriptorCollection props = TypeDescriptor.GetProperties(typeof(T));
            DataTable table = new DataTable();
            for (int i = 0 ; i < props.Count ; i++)
                {
                PropertyDescriptor prop = props[i];
                table.Columns.Add(prop.Name, prop.PropertyType);
                }

            object[] values = new object[props.Count];
            foreach (T item in data)
                {
                for (int i = 0 ; i < values.Length ; i++)
                    {
                    values[i] = props[i].GetValue(item);
                    }

                table.Rows.Add(values);
                }

            return table;
            }


        }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
[
    {
        "id": 7542826,
        "title": "Адрес 140",
        "address": "Адрес 140",
        "longitude": 05.961548,
        "latitude": 04.742206,
        "kkms": [
            {
                "id": 65231138,
                "orgId": 3122,
                "retailPlaceId": 7128126123,
                "internalName": "000423532061244",
                "model": "ZR2",
                "serialNumber": "0364589429",
                "onlineStatus": 1,
                "status": 4,
                "createdAt": 1580367599792,
                "declineReason": "",
                "fnsKkmId": "15975313354061244",
                "regStatus": "REGISTRATION_SUCCESS",
                "regStatusChangedAt": 165465370307890,
                "fiscalDrive": {
                    "producer": "",
                    "number": "54165453465469161",
                    "activationDate": 1580367540000
                },
                "lastShift": {
                    "kkmId": "5275257275061244",
                    "updateDate": 1752752752396335,
                    "shiftNumber": 187243,
                    "sells": 8895.61,
                    "returns": 0,
                    "lastTransaction": 1597252577410360000,
                    "shiftClosed": false,
                    "fiscalDriveReplaceRequired": false,
                    "fiscalDriveMemoryExceeded": false,
                    "fiscalDriveExhausted": false,
                    "fiscalDocumentsCount": 47,
                    "lastTransactionType": "TICKET"
                }
            },
            {
                "id": 657765130,
                "orgId": 3122,
                "retailPlaceId": 7128126123,
                "internalName": "000433185565000601",
                "model": "ZR2",
                "serialNumber": "08388284512361",
                "onlineStatus": 0,
                "status": 4,
                "createdAt": 1584534534534507,
                "declineReason": "",
                "fnsKkmId": "0004534534534530601",
                "regStatus": "REGISTRATION_SUCCESS",
                "regStatusChangedAt": 15453453453890,
                "fiscalDrive": {
                    "producer": "",
                    "number": "45354345354345566357",
                    "activationDate": 4534534534560000
                },
                "lastShift": {
                    "kkmId": "45354345345300601",
                    "updateDate": 1596094816137,
                    "shiftNumber": 1611,
                    "sells": 0,
                    "returns": 0,
                    "lastTransaction": 1597252577410360000,
                    "shiftClosed": false,
                    "fiscalDriveReplaceRequired": false,
                    "fiscalDriveMemoryExceeded": false,
                    "fiscalDriveExhausted": false,
                    "fiscalDocumentsCount": 0,
                    "lastTransactionType": "OPEN_SHIFT"
                }
            }
        ],
        "kkmsOnlineCount": 2
    }
]

In a previous blog post, we took a look at Java’s custom serialization platform and what the security implications are. And more recently, I wrote about how improvements in Java 17 can help you prevent insecure deserialization. However, nowadays, people aren’t as dependent on Java’s custom serialization, opting instead to use JSON. JSON is the most widespread format for data serialization, it is human readable and not specific to Java.

One of the most commonly used libraries is jackson-databind, which provides you with an ObjectMapper to transform your object into JSON and vice versa.

As it is a popular library, it’s very important to know that the jackson-databind library has been subject to many reported vulnerabilities over the past couple of years. Nevertheless, this does not mean that using the Jackson ObjectMapper is a security risk by default. This article will explain how the Jackson deserialization vulnerabilities work and how to make sure you are not affected by them.

But before we get too far, let’s answer a few questions you may have…

What is deserialization?

Deserialization is transforming the data from a file or stream back into an object to be used in your application. This can be binary data or structured data like JSON and XML. Deserialization is the opposite of serialization, which transforms objects into byte streams or structured text.

What is the Jackson ObjectMapper used for?

The Jackson Objectmapper is part of the Jackson databind library and is used the transform JSON into Java objects and vice versa. It is the most commonly used and well-known libraries in the Java ecosystem to convert JSON to and from Java, and it is automatically shipped with Spring Boot.

Is the Jackson Objectmapper secure?

Yes, the Jackson Objectmapper is secure and uses secure defaults. It is essential that you update to the most recent versions to avoid known security issues.

Is creating an ObjectMapper expensive?

Creating an ObjectMapper is quite expensive. Therefore it is recommended that you reuse your ObjectMapper instance. The Jackon ObjectMapper is thread-safe so it can safely be reused.

Using the Jackson ObjectMapper to create Java objects from JSON

With the code below,  we can create an ObjectMapper and use it to recreate a Person from a JSON string that comes from a file.

1ObjectMapper om = new ObjectMapper();
2Person myvalue = om.readValue(Files.readAllBytes(Paths.get("person.json")), Person.class);
3System.out.println("name:"+myvalue.name+" n"+"Age:"+myvalue.age);

This is all straightforward, and nothing really fancy is going to happen. Without any annotations, the Jackson ObjectMapper uses reflection to do the POJO mapping. Because of the reflection, it works on all fields regardless of the access modifier. If there are getters and setters available, the Jackson ObjectMapper will use that to do the mapping.

Default typing in Jackson

Many of the vulnerabilities with the Jackson library for JSON serialization depend on default typing, which is not enabled by default . You need to enable this explicitly. This means in most cases the vulnerabilities named in this list do not affect your system directly.

But let’s explain what default typing is and what it is used for.

Default typing is a mechanism in the Jackson ObjectMapper to deal with polymorphic types and inheritance. If you want to deserialize JSON to a Java POJO but are unsure what subtype the object or the field is, you can simply deserialize to the superclass.

Say you have Coffee and Tea. Both classes have the same superclass of HotDrink. So if your Breakfast contains a HotDrink but you are unaware if it is either Coffee or Tea you can use default typing to solve this.

1public class Breakfast {
2   public String food;
3   public HotDrink drink;
4}
5
6public abstract class HotDrink {
7   public String name;
8}
9
10public class Coffee extends HotDrink {
11   @Override
12   public String toString() {
13       return String.format("Coffee{name='%s'}", name);
14   }
15}
16
17public class Tea extends HotDrink {
18   @Override
19   public String toString() {
20       return String.format("Tea{name='%s'}", name);
21   }
22}
23
24String breakfastJson = """
25       {
26           "food":"sandwich",
27           "drink":["nl.brianvermeer.example.jackson.serialization.Tea",{"name":"oolong"}]
28       }
29       """;
30
31var om = new ObjectMapper();
32om.enableDefaultTyping();
33
34var myBreakfast = om.readValue(breakfastJson, Breakfast.class);
35System.out.println("breakfast hotdrink:"+myBreakfast.drink);

In this example, I enabled default typing on the ObjectMapper so that I can handle polymorphism everywhere I use this ObjectMapper. You can also do this on a specific field using the @JsonTypeInfo annotation.

Security problems with default typing on the Jackson ObjectMapper

So if default typing is enabled globally, it is possible to take inheritance to the extreme. If your Breakfast does not contain a HotDrink but a field of type Object, then any object can be available on the classpath. This also means we can deserialize any object that is available on the classpath. Potentially, this can be a gadget object that sets up a gadget chain and eventually ends in a remote execution.

These gadget chains are pretty similar to those described in my Serialization and deserialization in Java blog post. Let’s simplify this with a single gadget that executes a command right away when initialized. My SecondBreakfast class blow, containing a drink of type Object.

1public class Gadget {
2
3   private Runnable command;
4
5   public Gadget(String value) {
6       this.command = new Command(value);
7       this.command.run();
8   }
9}
10
11public class SecondBreakfast {
12   public String food;
13   public Object drink;
14}

If I deserialize my SecondBreakfast with default typing enabled, I can deserialize an Object containing an arbitrary code execution.

1String secondBreakfastJson = """
2       {
3           "food":"sandwich",
4           "drink":["nl.brianvermeer.example.jackson.serialization.Gadget", "rm -rf *"]
5       }
6       """;
7
8var om = new ObjectMapper();
9om.enableDefaultTyping();
10
11Var mySecondBreakfast = om.readValue(secondBreakfastJson, SecondBreakfast.class);
12System.out.println("Second breakfast hotdrink:"+mySecondBreakfast.drink);

Now this is a simplified example. But while finding and creating a gadget chain is not easy, it definitely is possible. Because of all the libraries and frameworks we use, chances are that a combination of all the classes in your classpath can be used to create such a gadget chain.

Also, there is already a large set of well-known «nasty classes» identified. Deserialization of such a class is considered dangerous and basically follows the pattern as described above. For reference, check the list of deserialization vulnerabilities on the jackson-databind library on the Snyk vulnerability database.

How does this impact my application?

Ok, don’t panic because it is not that bad. First of all, the maintainers of the jackson-databind library actively block the set of “nasty classes” in the SubTypeValidator. Secondly, you need to enable default types explicitly. This means by default, this setting is off, and polymorphic deserialization is not even possible.

Scanning with SCA tools like Snyk does show the vulnerability in the scan result. A quick search shows that there were a lot of these problems in the past. It is always wise to update to the newest version because the library is well maintained, and new “nasty classes” will be actively blocked when found. Nevertheless, the best way is to prevent enabling default typing of your Jackson ObjectMapper.

Snyk triage assistant to the rescue

Snyk is currently making an effort to filter these vulnerabilities if your code does not meet the prerequisites. For the Jackson ObjectMapper this means, if your code does not enable polymorphic typing, we will show you that it is unlikely that this specific vuln will exploit you. At the time of writing, this feature is only released for the Jackson deserialization vulnerability, but the team continues working on improving and expanding this feature. To use this feature, we need approval to scan your code to check if you do not have default typing enabled.

wordpress-sync/blog-jackson-objectmapper-snyk

The code examples used in this blog post are published on my GitHub account. Feel free to fork or reuse these examples in any way you prefer.

Deserialize safely!

The best way to avoid deserialization problems with the Jackson ObjectMapper is to prevent polymorphic typing. Please do not enable default typing for your ObjectMapper. Also, connect your project to Snyk to find out if you are using a jackson-databind library with known vulnerabilities. In most cases, you can easily replace it with a newer version. When you enable code scanning, the Triage Assistant can help you determine if it is likely or unlikely to be exploitable.

Понравилась статья? Поделить с друзьями:
  • Ошибка деретикс 9 гта са
  • Ошибка деполюшн систем фаулти пежо 308
  • Ошибка демонстрации рекламы код ошибки 0
  • Ошибка деловой презентации повторения
  • Ошибка делить на ноль excel