Tom
Tom

Reputation: 1241

json_decode to custom class

Is it possible to decode a json string to an object other than stdClass?

Upvotes: 112

Views: 103229

Answers (17)

Headbank
Headbank

Reputation: 400

You can't teach json_decode() about class definitions it knows nothing about, so you have to wrap it in your own function with your own logic to recognise those classes and convert the generic (array or stdClass) output into the instances you want.

If you have a range of classes you need to do this with, you can define a generic way of serialising them in a way that your decoding function can pick them out. In this example I use a trait that I apply to any classes I'm using:

trait JSONable {
    
    public function jsonSerialize(): mixed {
        return ['_cn' => static::class] + $this->toJSON();
    }
    
    protected function toJSON(): array {
        return (array) $this;
    }
    
    abstract protected static function fromJSON(array $data): static;
}

This implements the JsonSerializable interface; classes that use it must specify that for json_decode() to work on them as we want. It makes the JSON output have the classname added as the member "_cn" (it can be anything that won't collide with your real property names).

Classes must implement fromJSON() to instantiate an object with an associative array returned from our decoding function. If they want to do any of the custom things they would normally do in $this->jsonSerialize(), such as handling private/protected members in a nicer way, they can override toJSON().

Here's that decoding function:

function jsdecode(string $json,bool $associative=false,mixed &$out=null): mixed {
    $out ??= json_decode($json,$associative);
    $mark = '_cn';
    $decode = false;
    if($associative && is_array($out)) {
        foreach($out as $k => &$v) {
            jsdecode('',$associative,$v);
        }
        $decode = array_key_exists($mark,$out) && class_exists($out[$mark]);
    } elseif(!$associative && $out instanceof stdClass) {
        foreach(array_keys((array) $out) as $k) {
            jsdecode('',$associative,$out->$k);
        }
        if(property_exists($out,$mark) && class_exists($out->$mark)) {
            $decode = true;
            $out = (array) $out;
        }
    }
    if($decode) {
        $cn = $out[$mark];
        unset($out[$mark]);
        $out = $cn::fromJSON($out);
    }
    return $out;
}

Like json_decode() it is recursive, so if an applicable object contains other applicable objects, they too will be decoded as their correct class. It passes the $associative argument through to json_encode() in case you want to specify this for other parts of the data contained. It will puke if your class had private/protected members and you did not override toJSON() to deal with them.

Very simple example class and usage:

class Foo implements JsonSerializable {
  use JSONable;
  
  public $bar = 'bar';
  public $innerfoo;
  
  public static function fromJSON(array $data): static {
    $rtn = new static();
    $rtn->innerfoo = $data['innerfoo'];
    return $rtn;
  }
}

$f = new Foo();
$f->innerfoo = new Foo();

$json = json_encode([1,2,$f],true);

echo "Encoded form: ",$json,"\n";

$in = jsdecode($json);

var_dump($in);

I think this is the least messing-around with your target classes you can get away with, if you cannot predict which classes will occur in your JSON data structure, or where.

Upvotes: 0

Greald Henstra
Greald Henstra

Reputation: 576

All this here inspired me to a generic function:

function loadJSON($Obj, $json)
{
     $dcod = json_decode($json);
     $prop = get_object_vars ( $dcod );
     foreach($prop as $key => $lock)
     {
        if(property_exists ( $Obj ,  $key ))
        {
            if(is_object($dcod->$key))
            {
                loadJSON($Obj->$key, json_encode($dcod->$key));
            }
            else
            {
                $Obj->$key = $dcod->$key;
            }
        }
    }
    return $Obj;
}

to be called in class declaration:

class Bar{public $bar = " Boss";}
class Bas
{
    public $ber ;
    public $bas=" Boven"; 
    public function __construct() 
        {$this->ber = new Bar;}
}
class Baz
{
    public $bes ;
    public $baz=" Baaz";  
    public function __construct() 
        {$this->bes = new Bas;}
}

$Bazjson = '{"bes":{"ber":{"bar":"Baas"}}}';

$Bazobj = new Baz;

loadJSON($Bazobj, $Bazjson);
var_dump($Bazobj);

Upvotes: 0

dpi
dpi

Reputation: 2007

Not directly, but if the class has a constructor with parameter names that match the keys in the JSON object, you can simply decode the JSON into an associative array and pass it to the constructor via the '...' (argument unpacking) operator:

<?php
class MyClass {
    public function __construct(
        public int $id,
        public string $name,
        public array $attributes,
    ){}
}
$json = '{"name":"foo","id":42,"attributes":{"color":"red"}}';
$object = new MyClass(...json_decode($json, true));
print_r($object);

Output:

MyClass Object
(
    [id] => 42
    [name] => foo
    [attributes] => Array
        (
            [color] => red
        )

)

However, in practice, there is often some additional mapping to do, especially sub-objects that need to be recursively decoded too. So usually it is better to have a static fromArray function in each class that pre-processes the json-decoded array before passing the result to the constructor:

class Part {
    public function __construct(public float $weight){}
    public static function fromArray(array $data): self {
        return new self(...$data);
    }
}
class System {
    public function __construct(
        public string $name,
        public Part $mainPart,
        public array $otherParts,
    ){}
    public static function fromArray(array $data): self {
        $data['mainPart'] = Part::fromArray($data['mainPart']);
        $data['otherParts'] = array_map(Part::fromArray(...), $data['otherParts']); // php 8.1
        return new self(...$data);
    }
}
$json = '{"name":"foo","mainPart":{"weight":2},"otherParts":[{"weight":1}, {"weight":0.5}]}';
$object = System::fromArray(json_decode($json, true));

Upvotes: 0

Henry Nkuke
Henry Nkuke

Reputation: 1

This worked for me, especially for if you don't have setters or named properties in the target class

function cast($jsonstring, $class)
{
   //$class is a string like 'User'

    $json= json_decode($jsonstring,true);  //array
   
    $reflection = new ReflectionClass($class);
    $instance = $reflection->newInstanceWithoutConstructor();
    $keys = array_keys($json);

    foreach ($keys  as $key => $property) {
        $instance->{$property} =$json[$property];
    }

   // print_r($instance);

    return $instance;
}

Upvotes: 0

Peter Mghendi
Peter Mghendi

Reputation: 199

I went ahead and implemented John Petit's answer, as a function(gist):

function json_decode_to(string $json, string $class = stdClass::class, int $depth = 512, int $options = 0)
{
    $stdObj = json_decode($json, false, $depth, $options);
    if ($class === stdClass::class) return $stdObj;

    $count = strlen($class);
    $temp = serialize($stdObj);
    $temp = preg_replace("@^O:8:\"stdClass\":@", "O:$count:\"$class\":", $temp);
    return unserialize($temp);  
}

This worked perfectly for my use case. However Yevgeniy Afanasyev's response seems equally promising to me. It could be possible to have your class have an extra "constructor", like so:

public static function withJson(string $json) {
    $instance = new static();
    // Do your thing
    return $instance;
}

This is also inspired by this answer.

EDIT: I have been using karriereat/json-decoder for some time now, and I have had absolutely no trouble with it. It is lightweight and very easily extensible. Here's an example of a binding I wrote to deserialize JSON into a Carbon/CarbonImmutable object.

Upvotes: 0

luke23489
luke23489

Reputation: 49

Use Reflection:

function json_decode_object(string $json, string $class)
{
    $reflection = new ReflectionClass($class);
    $instance = $reflection->newInstanceWithoutConstructor();
    $json = json_decode($json, true);
    $properties = $reflection->getProperties();
    foreach ($properties as $key => $property) {
        $property->setAccessible(true);
        $property->setValue($instance, $json[$property->getName()]);
    }
    return $instance;
}

Upvotes: 4

Lucas Bustamante
Lucas Bustamante

Reputation: 17188

I'm surprised no one mentioned this, yet.

Use the Symfony Serializer component: https://symfony.com/doc/current/components/serializer.html

Serializing from Object to JSON:

use App\Model\Person;

$person = new Person();
$person->setName('foo');
$person->setAge(99);
$person->setSportsperson(false);

$jsonContent = $serializer->serialize($person, 'json');

// $jsonContent contains {"name":"foo","age":99,"sportsperson":false,"createdAt":null}

echo $jsonContent; // or return it in a Response

Deserializing from JSON to Object: (this example uses XML just to demonstrate the flexibility of formats)

use App\Model\Person;

$data = <<<EOF
<person>
    <name>foo</name>
    <age>99</age>
    <sportsperson>false</sportsperson>
</person>
EOF;

$person = $serializer->deserialize($data, Person::class, 'xml');

Upvotes: 14

Malachi
Malachi

Reputation: 33700

You could use Johannes Schmitt's Serializer library.

$serializer = JMS\Serializer\SerializerBuilder::create()->build();
$object = $serializer->deserialize($jsonData, 'MyNamespace\MyObject', 'json');

In the latest version of the JMS serializer the syntax is:

$serializer = SerializerBuilder::create()->build();
$object = $serializer->deserialize($jsonData, MyObject::class, 'json');

Upvotes: 20

John Pettitt
John Pettitt

Reputation: 659

You can do it - it's a kludge but totally possible. We had to do when we started storing things in couchbase.

$stdobj = json_decode($json_encoded_myClassInstance);  //JSON to stdClass
$temp = serialize($stdobj);                   //stdClass to serialized

// Now we reach in and change the class of the serialized object
$temp = preg_replace('@^O:8:"stdClass":@','O:7:"MyClass":',$temp);

// Unserialize and walk away like nothing happend
$myClassInstance = unserialize($temp);   // Presto a php Class 

In our benchmarks this was way faster than trying to iterate through all the class variables.

Caveat: Won't work for nested objects other than stdClass

Edit: keep in mind the data source, it's strongly recommended that you don't do this withe untrusted data from users without a very carful analysis of the risks.

Upvotes: 30

Yevgeniy Afanasyev
Yevgeniy Afanasyev

Reputation: 41330

You can make a wrapper for your object and make the wrapper look like it is the object itself. And it will work with multilevel objects.

<?php
class Obj
{
    public $slave;

    public function __get($key) {
        return property_exists ( $this->slave ,  $key ) ? $this->slave->{$key} : null;
    }

    public function __construct(stdClass $slave)
    {
        $this->slave = $slave;
    }
}

$std = json_decode('{"s3":{"s2":{"s1":777}}}');

$o = new Obj($std);

echo $o->s3->s2->s1; // you will have 777

Upvotes: 5

jigarshahindia
jigarshahindia

Reputation: 79

You can do it in below way ..

<?php
class CatalogProduct
{
    public $product_id;
    public $sku;
    public $name;
    public $set;
    public $type;
    public $category_ids;
    public $website_ids;

    function __construct(array $data) 
    {
        foreach($data as $key => $val)
        {
            if(property_exists(__CLASS__,$key))
            {
                $this->$key =  $val;
            }
        }
    }
}

?>

For more details visit create-custom-class-in-php-from-json-or-array

Upvotes: 6

cweiske
cweiske

Reputation: 31078

We built JsonMapper to map JSON objects onto our own model classes automatically. It works fine with nested/child objects.

It only relies on docblock type information for mapping, which most class properties have anyway:

<?php
$mapper = new JsonMapper();
$contactObject = $mapper->map(
    json_decode(file_get_contents('http://example.org/contact.json')),
    new Contact()
);
?>

Upvotes: 50

klawipo
klawipo

Reputation: 153

I once created an abstract base class for this purpose. Let's call it JsonConvertible. It should serialize and deserialize the public members. This is possible using Reflection and late static binding.

abstract class JsonConvertible {
   static function fromJson($json) {
       $result = new static();
       $objJson = json_decode($json);
       $class = new \ReflectionClass($result);
       $publicProps = $class->getProperties(\ReflectionProperty::IS_PUBLIC);
       foreach ($publicProps as $prop) {
            $propName = $prop->name;
            if (isset($objJson->$propName) {
                $prop->setValue($result, $objJson->$propName);
            }
            else {
                $prop->setValue($result, null);
            }
       }
       return $result;
   }
   function toJson() {
      return json_encode($this);
   }
} 

class MyClass extends JsonConvertible {
   public $name;
   public $whatever;
}
$mine = MyClass::fromJson('{"name": "My Name", "whatever": "Whatever"}');
echo $mine->toJson();

Just from memory, so probably not flawless. You will also have to exclude static properties and may give derived classes the chance to make some properties ignored when serialized to/from json. I hope you get the idea, nonetheless.

Upvotes: 1

Michael McTiernan
Michael McTiernan

Reputation: 5313

Not automatically. But you can do it the old fashioned route.

$data = json_decode($json, true);

$class = new Whatever();
foreach ($data as $key => $value) $class->{$key} = $value;

Or alternatively, you could make that more automatic:

class Whatever {
    public function set($data) {
        foreach ($data AS $key => $value) $this->{$key} = $value;
    }
}

$class = new Whatever();
$class->set($data);

Edit: getting a little fancier:

class JSONObject {
    public function __construct($json = false) {
        if ($json) $this->set(json_decode($json, true));
    }

    public function set($data) {
        foreach ($data AS $key => $value) {
            if (is_array($value)) {
                $sub = new JSONObject;
                $sub->set($value);
                $value = $sub;
            }
            $this->{$key} = $value;
        }
    }
}

// These next steps aren't necessary. I'm just prepping test data.
$data = array(
    "this" => "that",
    "what" => "who",
    "how" => "dy",
    "multi" => array(
        "more" => "stuff"
    )
);
$jsonString = json_encode($data);

// Here's the sweetness.
$class = new JSONObject($jsonString);
print_r($class);

Upvotes: 119

Gordon
Gordon

Reputation: 316969

No, this is not possible as of PHP 5.5.1.

The only thing possible is to have json_decode return associate arrays instead of the StdClass objects.

Upvotes: 3

Francesco Terenzani
Francesco Terenzani

Reputation: 1391

As Gordon says is not possible. But if you are looking for a way to obtain a string that can be decoded as an instance of a give class you can use serialize and unserialize instead.

class Foo
{

    protected $bar = 'Hello World';

    function getBar() {
        return $this->bar;
    }

}

$string = serialize(new Foo);

$foo = unserialize($string);
echo $foo->getBar();

Upvotes: 1

ThiefMaster
ThiefMaster

Reputation: 318498

JSON is a simple protocol to transfer data between various programming languages (and it's also a subset of JavaScript) which supports just certain types: numbers, strings, arrays/lists, objects/dicts. Objects are just key=value maps and Arrays are ordered lists.

So there is no way to express custom objects in a generic way. The solution is defining a structure where your program(s) will know that it's a custom object.

Here's an example:

{ "cls": "MyClass", fields: { "a": 123, "foo": "bar" } }

This could be used to create an instance of MyClass and set the fields a and foo to 123 and "bar".

Upvotes: 0

Related Questions