📜 ⬆️ ⬇️

Elm. Comfortable and awkward. Json.Encoder and Json.Decoder

We continue to talk about Elm 0.18 .


Elm. Comfortable and awkward
Elm. Comfortable and awkward. Composition
Elm. Comfortable and awkward. Http, Task


In this article we will consider the issues of encoders / decoders.


Decoders / Encoders are used for:


  1. conversion of responses from third-party resources (Http, WebSocket, etc.);
  2. port interactions. In more detail about ports and the native code I will tell in the following articles.

As described earlier, Elm requires us to convert external data into internal application types. The Json.Decode module is responsible for this process. Reverse process - Json.Encode .


The type that defines decoding rules is Json.Decode.Decoder a . This type is parameterized by the user type and determines how to get the user type a from the JSON object.


For the encoder, only the result type is defined - Json.Encode.Value .


Consider examples for the type UserData.


type alias User = { id: Int , name: String , email: String } 

Decoder for receiving user data:


 decodeUserData : Json.Decode.Decoder UserData decodeUserData = Json.Decode.map3 UserData (Json.Decode.field “id” Json.Decode.int) (Json.Decode.field “name” Json.Decode.string) (Json.Decode.field “email” Json.Decode.string) encodeUserData : UserData -> Json.Encode.Value encodeUserData userData = Json.Encode.object [ ( “id”, Json.Encode.int userData.id) , ( “name”, Json.Encode.string userData.name) , ( “email”, Json.Encode.string userData.email) ] 

The Json.Decode.map3 function accepts a UserData type constructor. Next, three type decoders are transmitted according to the order in which they are declared in the UserData user type.


The decodeUserData function can be used in conjunction with the Json.Decode.decodeString or Json.Decode.decodeValue functions. Example of use from previous articles.


The encodeUserData function encodes a custom type into a Json.Encode.Value type, which can be sent out. By simple, Json.Encode.Value corresponds to a JSON object.


Simple options are described in the documentation, they can be studied without much difficulty. Let's look at life situations that require some finger dexterity.


Union type decoders or type discriminators


Suppose we have a catalog of goods. And each product can have an arbitrary number of attributes, each of which is of the type one of the set:


  1. integer;
  2. line;
  3. enumerated. Assumes a choice of one of the valid values.

JSON object is of the following form:


 { “id”: 1, “name”: “Product name”, “price”: 1000, “attributes”: [ { “id”: 1, “name”: “Length”, “unit”: “meters”, “value”: 100 }, { “id”: 1, “name”: “Color”, “unit”: “”, “value”: { “id”: 1, “label”: “red” } },... ] } 

The remaining possible types will not be considered, working with them is similar. Then a custom item type would have the following description:


 type alias Product = { id: Int , name: String , price: Int , attributes: Attributes } type alias Attributes = List Attribute type alias Attribute = { id: Int , name: String , unit: String , value: AttributeValue } type AttributeValue = IntValue Int | StringValue String | EnumValue Enum type alias Enum = { id: Int , label: String } 

Lightly discuss the types described. There is a product (Product) that contains a list of attributes / characteristics (Attributes). Each attribute (Attribute) contains an identifier, a name, a dimension, and a value. An attribute value is described as a union type, one for each type of characteristic value. The Enum type describes one value from the allowed set and contains: an identifier and a human readable value.


Description of the decoder, the prefix Json.Decode omitted for brevity:


 decodeProduct : Decoder Product decodeProduct = map4 Product (field “id” int) (field “name” string) (field “price” int) (field “attributes” decodeAttributes) decodeAttributes : Decoder Attributes decodeAttributes = list decodeAttribute decodeAttribute : Decoder Attribute decodeAttribute = map4 Attribute (field “id” int) (field “name” string) (field “unit” string) (field “value” decodeAttributeValue) decodeAttributeValue : Decoder AttributeValue decodeAttributeValue = oneOf [ map IntValue int , map StringValue string , map EnumValue decodeEnumValue ] decodeEnumValue : Decoder Enum decodeEnumValue = map2 Enum (field “id” int) (field “label” string) 

The whole trick is contained in the decodeAttributeValue function. Using the Json.Decode.oneOf function, all valid decoders for an attribute value are searched. In case of successful decompression by one of the decoders, the value is tagged with the corresponding tag from the AttributeValue type.


The encoding of the Product type can be performed using the Json.Encode.object function, to which the encoded type attributes will be passed. It is worth paying attention to the coding of the AttributeValue type. In accordance with the previously described JSON object, the encoder can be described as, the prefix Json.Encode is omitted for brevity:


 encodeAttributeValue : AttributeValue -> Value encodeAttributeValue attributeValue = case attributeValue of IntValue value -> int value StringValue value -> string value EnumValue value -> object [ (“id”, int value.id) , (“id”, string value.label) ] 

As you can see, we compare the type options and use the appropriate encoders.


Let's change the description of attributes and define them using a type discriminator. The attribute JSON object, in this case, would look like this:


 { “id”: 1, “name”: “Attribute name”, “type”: “int”, “value_int”: 1, “value_string”: null, “value_enum_id”: null, “value_enum_label”: null } 

In this case, the type discriminator is stored in the type field and determines in which field the value is stored. Such a description structure is probably not the most convenient, but often encountered. Whether it is worth changing the type description for this JSON object is probably not worth it; it is better to keep the types in a convenient form for internal use. In this case, the description of the decoder may be as follows:


 decodeAttribute2 : Decoder Attribute decodeAttribute2 = field "type" string |> andThen decodeAttributeValueType |> andThen (\attributeValue -> map4 Attribute (field "id" int) (field "name" string) (field "unit" string) (succeed attributeValue) ) decodeAttributeValueType : String -> Decoder AttributeValue decodeAttributeValueType valueType = case valueType of "int" -> field "value_int" int |> Json.Decode.map IntValue "string" -> field "value_string" string |> Json.Decode.map StringValue "enum" -> map2 Enum (field "value_enum_id" int) (field "value_enum_label" string) |> Json.Decode.map EnumValue _ -> Json.Decode.fail "Unknown attribute type" 

In the decodeAttribute2 function, we first decode the discriminator; in case of success, we decode the attribute value. Next, we decode the remaining fields of the Attribute type, and specify the previously obtained value as the value of the value field.


Source code decoder .


Partial type update


There are cases when the API does not return the entire object, but only a part of it. For example, when registering to view or change the status of the object. In this case, in the message it is more convenient to immediately receive the updated object, and to hide all manipulations behind the decoder.


For example, take the same product, but add a status field to it and process the request to close the product.


 type alias Product = { id: Int , name: String , price: Int , attributes: Attributes , status: Int } decodeUpdateStatus : Product -> Decoder Product decodeUpdateStatus product = field “status” int |> andThen (\newStatus -> succeed { product | status = newStatus} ) 

Or you can use the Json.Decode.map function.


 decodeUpdateStatus : Product -> Decoder Product decodeUpdateStatus product = field “status” int |> map (\newStatus -> { product | status = newStatus} ) 

date and time


We will use the Date.fromString function, which is implemented using a Date type constructor.


 decodeDateFromString : Decoder Date.Date decodeDateFromString = string |> andThen (\stringDate -> case Date.fromString stringDate of Ok date -> Json.Decode.succeed date Err reason -> Json.Decode.fail reason ) 

If the Timestamp is used as the date / time representation, then the decoder in general can be described as:


 decodeDateFromTimestamp : Decoder Date.Date decodeDateFromTimestamp = oneOf [ int |> Json.Decode.map toFloat , float ] |> Json.Decode.map Date.fromTime 

')

Source: https://habr.com/ru/post/424437/


All Articles